1111class Tests_Query_DeterministicOrdering extends WP_UnitTestCase {
1212
1313 /**
14- * Test that deterministic ordering prevents duplicate records across pages .
14+ * Post IDs for posts with identical dates (for date ordering tests) .
1515 *
16- * This is the core test for the bug fix. When multiple posts have the same
17- * value for a field (like post_date), pagination can show duplicate records
18- * without deterministic ordering.
16+ * @var array
17+ */
18+ protected static $ date_identical_post_ids = array ();
19+
20+ /**
21+ * Post IDs for posts with identical titles (for title ordering tests).
1922 *
20- * @ticket xxxxx
23+ * @var array
2124 */
22- public function test_deterministic_ordering_prevents_duplicates_across_pages () {
23- // Create multiple posts with identical post_date to trigger the bug
24- $ identical_date = '2023-01-01 10:00:00 ' ;
25- $ post_ids = array ();
25+ protected static $ title_identical_post_ids = array ();
26+
27+ /**
28+ * Post IDs for search tests.
29+ *
30+ * @var array
31+ */
32+ protected static $ search_post_ids = array ();
33+
34+ /**
35+ * Post IDs for menu_order tests.
36+ *
37+ * @var array
38+ */
39+ protected static $ menu_order_post_ids = array ();
40+
41+ /**
42+ * Set up shared fixtures for all tests.
43+ */
44+ public static function set_up_before_class () {
45+ parent ::set_up_before_class ();
2646
47+ // Register custom post types for test isolation.
48+ register_post_type (
49+ 'wptests_time_ident ' ,
50+ array (
51+ 'public ' => true ,
52+ )
53+ );
54+
55+ register_post_type (
56+ 'wptests_title_ident ' ,
57+ array (
58+ 'public ' => true ,
59+ )
60+ );
61+
62+ // Create posts with identical dates for date ordering tests.
63+ $ identical_date = '2023-01-01 10:00:00 ' ;
2764 for ( $ i = 1 ; $ i <= 20 ; $ i ++ ) {
28- $ post_ids [] = self ::factory ()->post ->create (
65+ self :: $ date_identical_post_ids [] = self ::factory ()->post ->create (
2966 array (
67+ 'post_type ' => 'wptests_time_ident ' ,
3068 'post_title ' => "Post $ i " ,
3169 'post_date ' => $ identical_date ,
3270 )
3371 );
3472 }
3573
74+ // Create posts with identical titles for title ordering tests.
75+ $ identical_title = 'Same Title ' ;
76+ for ( $ i = 1 ; $ i <= 15 ; $ i ++ ) {
77+ self ::$ title_identical_post_ids [] = self ::factory ()->post ->create (
78+ array (
79+ 'post_type ' => 'wptests_title_ident ' ,
80+ 'post_title ' => $ identical_title ,
81+ 'post_date ' => '2023-01- ' . str_pad ( (string ) $ i , 2 , '0 ' , STR_PAD_LEFT ) . ' 10:00:00 ' ,
82+ )
83+ );
84+ }
85+
86+ // Create posts for search tests.
87+ $ identical_date = '2023-01-01 10:00:00 ' ;
88+ for ( $ i = 1 ; $ i <= 12 ; $ i ++ ) {
89+ self ::$ search_post_ids [] = self ::factory ()->post ->create (
90+ array (
91+ 'post_type ' => 'wptests_time_ident ' ,
92+ 'post_title ' => "Test Post $ i " ,
93+ 'post_content ' => 'This is a test post ' ,
94+ 'post_date ' => $ identical_date ,
95+ )
96+ );
97+ }
98+
99+ // Create pages with identical menu_order for menu_order tests.
100+ for ( $ i = 1 ; $ i <= 20 ; $ i ++ ) {
101+ self ::$ menu_order_post_ids [] = self ::factory ()->post ->create (
102+ array (
103+ 'post_type ' => 'page ' ,
104+ 'post_title ' => "Page $ i " ,
105+ 'menu_order ' => 0 , // All pages have same menu_order
106+ )
107+ );
108+ }
109+ }
110+
111+ /**
112+ * Clean up after all tests.
113+ */
114+ public static function tear_down_after_class () {
115+ _unregister_post_type ( 'wptests_time_ident ' );
116+ _unregister_post_type ( 'wptests_title_ident ' );
117+
118+ self ::$ date_identical_post_ids = array ();
119+ self ::$ title_identical_post_ids = array ();
120+ self ::$ search_post_ids = array ();
121+ self ::$ menu_order_post_ids = array ();
122+
123+ parent ::tear_down_after_class ();
124+ }
125+
126+ /**
127+ * Test that deterministic ordering prevents duplicate records across pages.
128+ *
129+ * This is the core test for the bug fix. When multiple posts have the same
130+ * value for a field (like post_date), pagination can show duplicate records
131+ * without deterministic ordering.
132+ *
133+ * @ticket xxxxx
134+ */
135+ public function test_deterministic_ordering_prevents_duplicates_across_pages () {
136+ // Use shared fixtures with identical post_date
137+
36138 // Get first page
37139 $ query1 = new WP_Query (
38140 array (
141+ 'post_type ' => 'wptests_time_ident ' ,
142+ 'post__in ' => self ::$ date_identical_post_ids ,
39143 'orderby ' => 'post_date ' ,
40144 'order ' => 'ASC ' ,
41145 'posts_per_page ' => 10 ,
@@ -46,6 +150,8 @@ public function test_deterministic_ordering_prevents_duplicates_across_pages() {
46150 // Get second page
47151 $ query2 = new WP_Query (
48152 array (
153+ 'post_type ' => 'wptests_time_ident ' ,
154+ 'post__in ' => self ::$ date_identical_post_ids ,
49155 'orderby ' => 'post_date ' ,
50156 'order ' => 'ASC ' ,
51157 'posts_per_page ' => 10 ,
@@ -68,6 +174,8 @@ public function test_deterministic_ordering_prevents_duplicates_across_pages() {
68174 // Verify deterministic ordering: same query should return same results
69175 $ query1_repeat = new WP_Query (
70176 array (
177+ 'post_type ' => 'wptests_time_ident ' ,
178+ 'post__in ' => self ::$ date_identical_post_ids ,
71179 'orderby ' => 'post_date ' ,
72180 'order ' => 'ASC ' ,
73181 'posts_per_page ' => 10 ,
@@ -85,21 +193,12 @@ public function test_deterministic_ordering_prevents_duplicates_across_pages() {
85193 * @ticket xxxxx
86194 */
87195 public function test_deterministic_ordering_with_post_title () {
88- $ identical_title = 'Same Title ' ;
89- $ post_ids = array ();
90-
91- for ( $ i = 1 ; $ i <= 15 ; $ i ++ ) {
92- $ post_ids [] = self ::factory ()->post ->create (
93- array (
94- 'post_title ' => $ identical_title ,
95- 'post_date ' => "2023-01- " . str_pad ((string ) $ i , 2 , '0 ' , STR_PAD_LEFT ) . " 10:00:00 " ,
96- )
97- );
98- }
99-
196+ // Use shared fixtures with identical post_title
100197 // Get first page
101198 $ query1 = new WP_Query (
102199 array (
200+ 'post_type ' => 'wptests_title_ident ' ,
201+ 'post__in ' => self ::$ title_identical_post_ids ,
103202 'orderby ' => 'post_title ' ,
104203 'order ' => 'ASC ' ,
105204 'posts_per_page ' => 8 ,
@@ -110,6 +209,8 @@ public function test_deterministic_ordering_with_post_title() {
110209 // Get second page
111210 $ query2 = new WP_Query (
112211 array (
212+ 'post_type ' => 'wptests_title_ident ' ,
213+ 'post__in ' => self ::$ title_identical_post_ids ,
113214 'orderby ' => 'post_title ' ,
114215 'order ' => 'ASC ' ,
115216 'posts_per_page ' => 8 ,
@@ -131,21 +232,12 @@ public function test_deterministic_ordering_with_post_title() {
131232 * @ticket xxxxx
132233 */
133234 public function test_deterministic_ordering_with_desc_order () {
134- $ identical_date = '2023-01-01 10:00:00 ' ;
135- $ post_ids = array ();
136-
137- for ( $ i = 1 ; $ i <= 12 ; $ i ++ ) {
138- $ post_ids [] = self ::factory ()->post ->create (
139- array (
140- 'post_title ' => "Post $ i " ,
141- 'post_date ' => $ identical_date ,
142- )
143- );
144- }
145-
235+ // Use shared fixtures with identical post_date
146236 // Get first page with DESC order
147237 $ query1 = new WP_Query (
148238 array (
239+ 'post_type ' => 'wptests_time_ident ' ,
240+ 'post__in ' => self ::$ date_identical_post_ids ,
149241 'orderby ' => 'post_date ' ,
150242 'order ' => 'DESC ' ,
151243 'posts_per_page ' => 6 ,
@@ -156,6 +248,8 @@ public function test_deterministic_ordering_with_desc_order() {
156248 // Get second page with DESC order
157249 $ query2 = new WP_Query (
158250 array (
251+ 'post_type ' => 'wptests_time_ident ' ,
252+ 'post__in ' => self ::$ date_identical_post_ids ,
159253 'orderby ' => 'post_date ' ,
160254 'order ' => 'DESC ' ,
161255 'posts_per_page ' => 6 ,
@@ -177,21 +271,12 @@ public function test_deterministic_ordering_with_desc_order() {
177271 * @ticket xxxxx
178272 */
179273 public function test_deterministic_ordering_with_array_orderby () {
180- $ identical_date = '2023-01-01 10:00:00 ' ;
181- $ post_ids = array ();
182-
183- for ( $ i = 1 ; $ i <= 16 ; $ i ++ ) {
184- $ post_ids [] = self ::factory ()->post ->create (
185- array (
186- 'post_title ' => "Post $ i " ,
187- 'post_date ' => $ identical_date ,
188- )
189- );
190- }
191-
274+ // Use shared fixtures with identical post_date
192275 // Test with array orderby
193276 $ query1 = new WP_Query (
194277 array (
278+ 'post_type ' => 'wptests_time_ident ' ,
279+ 'post__in ' => self ::$ date_identical_post_ids ,
195280 'orderby ' => array (
196281 'post_date ' => 'ASC ' ,
197282 'post_title ' => 'ASC ' ,
@@ -203,6 +288,8 @@ public function test_deterministic_ordering_with_array_orderby() {
203288
204289 $ query2 = new WP_Query (
205290 array (
291+ 'post_type ' => 'wptests_time_ident ' ,
292+ 'post__in ' => self ::$ date_identical_post_ids ,
206293 'orderby ' => array (
207294 'post_date ' => 'ASC ' ,
208295 'post_title ' => 'ASC ' ,
@@ -226,20 +313,11 @@ public function test_deterministic_ordering_with_array_orderby() {
226313 * @ticket xxxxx
227314 */
228315 public function test_deterministic_ordering_does_not_duplicate_id () {
229- $ identical_date = '2023-01-01 10:00:00 ' ;
230- $ post_ids = array ();
231-
232- for ( $ i = 1 ; $ i <= 10 ; $ i ++ ) {
233- $ post_ids [] = self ::factory ()->post ->create (
234- array (
235- 'post_title ' => "Post $ i " ,
236- 'post_date ' => $ identical_date ,
237- )
238- );
239- }
240-
316+ // Use shared fixtures with identical post_date
241317 $ query = new WP_Query (
242318 array (
319+ 'post_type ' => 'wptests_time_ident ' ,
320+ 'post__in ' => self ::$ date_identical_post_ids ,
243321 'orderby ' => 'ID ' ,
244322 'order ' => 'ASC ' ,
245323 'posts_per_page ' => 10 ,
@@ -257,22 +335,12 @@ public function test_deterministic_ordering_does_not_duplicate_id() {
257335 * @ticket xxxxx
258336 */
259337 public function test_deterministic_ordering_with_search () {
260- $ identical_date = '2023-01-01 10:00:00 ' ;
261- $ post_ids = array ();
262-
263- for ( $ i = 1 ; $ i <= 12 ; $ i ++ ) {
264- $ post_ids [] = self ::factory ()->post ->create (
265- array (
266- 'post_title ' => "Test Post $ i " ,
267- 'post_content ' => 'This is a test post ' ,
268- 'post_date ' => $ identical_date ,
269- )
270- );
271- }
272-
338+ // Use shared fixtures for search tests
273339 // Test with search
274340 $ query1 = new WP_Query (
275341 array (
342+ 'post_type ' => 'wptests_time_ident ' ,
343+ 'post__in ' => self ::$ search_post_ids ,
276344 's ' => 'test ' ,
277345 'orderby ' => 'post_date ' ,
278346 'order ' => 'ASC ' ,
@@ -283,6 +351,8 @@ public function test_deterministic_ordering_with_search() {
283351
284352 $ query2 = new WP_Query (
285353 array (
354+ 'post_type ' => 'wptests_time_ident ' ,
355+ 'post__in ' => self ::$ search_post_ids ,
286356 's ' => 'test ' ,
287357 'orderby ' => 'post_date ' ,
288358 'order ' => 'ASC ' ,
@@ -298,4 +368,63 @@ public function test_deterministic_ordering_with_search() {
298368 $ overlap = array_intersect ( $ page1_ids , $ page2_ids );
299369 $ this ->assertEmpty ( $ overlap , 'Pages should not contain duplicate posts even with search ' );
300370 }
371+
372+ /**
373+ * Test that deterministic ordering works with menu_order field.
374+ *
375+ * @ticket xxxxx
376+ */
377+ public function test_deterministic_ordering_with_menu_order () {
378+ // Use shared fixtures with identical menu_order
379+ // Get first page
380+ $ query1 = new WP_Query (
381+ array (
382+ 'post_type ' => 'page ' ,
383+ 'post__in ' => self ::$ menu_order_post_ids ,
384+ 'orderby ' => 'menu_order ' ,
385+ 'order ' => 'ASC ' ,
386+ 'posts_per_page ' => 10 ,
387+ 'paged ' => 1 ,
388+ )
389+ );
390+
391+ // Get second page
392+ $ query2 = new WP_Query (
393+ array (
394+ 'post_type ' => 'page ' ,
395+ 'post__in ' => self ::$ menu_order_post_ids ,
396+ 'orderby ' => 'menu_order ' ,
397+ 'order ' => 'ASC ' ,
398+ 'posts_per_page ' => 10 ,
399+ 'paged ' => 2 ,
400+ )
401+ );
402+
403+ $ page1_ids = wp_list_pluck ( $ query1 ->posts , 'ID ' );
404+ $ page2_ids = wp_list_pluck ( $ query2 ->posts , 'ID ' );
405+
406+ // Verify no overlap between pages (no duplicates)
407+ $ overlap = array_intersect ( $ page1_ids , $ page2_ids );
408+ $ this ->assertEmpty ( $ overlap , 'Pages should not contain duplicate posts when ordering by menu_order ' );
409+
410+ // Verify total count is correct
411+ $ this ->assertEquals ( 20 , $ query1 ->found_posts , 'Total pages should be 20 ' );
412+ $ this ->assertEquals ( 10 , count ( $ page1_ids ), 'First page should have 10 pages ' );
413+ $ this ->assertEquals ( 10 , count ( $ page2_ids ), 'Second page should have 10 pages ' );
414+
415+ // Verify deterministic ordering: same query should return same results
416+ $ query1_repeat = new WP_Query (
417+ array (
418+ 'post_type ' => 'page ' ,
419+ 'post__in ' => self ::$ menu_order_post_ids ,
420+ 'orderby ' => 'menu_order ' ,
421+ 'order ' => 'ASC ' ,
422+ 'posts_per_page ' => 10 ,
423+ 'paged ' => 1 ,
424+ )
425+ );
426+ $ page1_repeat_ids = wp_list_pluck ( $ query1_repeat ->posts , 'ID ' );
427+
428+ $ this ->assertEquals ( $ page1_ids , $ page1_repeat_ids , 'Same query should return same results when ordering by menu_order ' );
429+ }
301430}
0 commit comments