@@ -245,6 +245,128 @@ protected function _test_list_hierarchical_page( array $args, array $expected_id
245245 }
246246 }
247247
248+ /**
249+ * Tests that a top-level page link has an aria-label with the title and "(Edit)".
250+ *
251+ * @ticket 62006
252+ *
253+ * @covers WP_Posts_List_Table::column_title
254+ */
255+ public function test_top_level_page_aria_label () {
256+ $ admin_id = self ::factory ()->user ->create ( array ( 'role ' => 'administrator ' ) );
257+ wp_set_current_user ( $ admin_id );
258+
259+ $ post = self ::$ top [1 ];
260+ $ title = apply_filters ( 'the_title ' , $ post ->post_title , $ post ->ID );
261+
262+ // level=0 → top-level page.
263+ ob_start ();
264+ $ this ->table ->single_row ( $ post , 0 );
265+ $ output = ob_get_clean ();
266+
267+ // Expected: "Title" (Edit) — using curly quotes as the translatable string uses “/”.
268+ $ this ->assertStringContainsString ( 'aria-label= ' , $ output );
269+ $ this ->assertStringContainsString ( esc_attr ( $ title ), $ output );
270+ $ this ->assertStringContainsString ( '(Edit) ' , html_entity_decode ( $ output , ENT_QUOTES | ENT_HTML5 ) );
271+ $ this ->assertStringNotContainsString ( 'subpage ' , $ output );
272+
273+ wp_set_current_user ( 0 );
274+ }
275+
276+ /**
277+ * Tests that a child page link includes "subpage of" with the parent page title in its aria-label.
278+ *
279+ * @ticket 62006
280+ *
281+ * @covers WP_Posts_List_Table::column_title
282+ */
283+ public function test_child_page_aria_label_includes_parent_name () {
284+ $ admin_id = self ::factory ()->user ->create ( array ( 'role ' => 'administrator ' ) );
285+ wp_set_current_user ( $ admin_id );
286+
287+ $ child = self ::$ children [1 ][1 ];
288+ $ parent = self ::$ top [1 ];
289+ $ child_title = apply_filters ( 'the_title ' , $ child ->post_title , $ child ->ID );
290+ $ parent_title = apply_filters ( 'the_title ' , $ parent ->post_title , $ parent ->ID );
291+
292+ // level=1 → direct child page.
293+ ob_start ();
294+ $ this ->table ->single_row ( $ child , 1 );
295+ $ output = ob_get_clean ();
296+
297+ $ decoded = html_entity_decode ( $ output , ENT_QUOTES | ENT_HTML5 );
298+
299+ $ this ->assertStringContainsString ( 'aria-label= ' , $ output );
300+ $ this ->assertStringContainsString ( 'subpage of ' , $ decoded );
301+ $ this ->assertStringContainsString ( $ child_title , $ decoded );
302+ $ this ->assertStringContainsString ( $ parent_title , $ decoded );
303+ $ this ->assertStringContainsString ( '(Edit) ' , $ decoded );
304+
305+ wp_set_current_user ( 0 );
306+ }
307+
308+ /**
309+ * Tests that a grandchild page link includes "subpage of" with its immediate parent title in its aria-label.
310+ *
311+ * @ticket 62006
312+ *
313+ * @covers WP_Posts_List_Table::column_title
314+ */
315+ public function test_grandchild_page_aria_label_includes_immediate_parent_name () {
316+ $ admin_id = self ::factory ()->user ->create ( array ( 'role ' => 'administrator ' ) );
317+ wp_set_current_user ( $ admin_id );
318+
319+ $ grandchild = self ::$ grandchildren [3 ][1 ][1 ];
320+ $ child = self ::$ children [3 ][1 ];
321+ $ grandchild_title = apply_filters ( 'the_title ' , $ grandchild ->post_title , $ grandchild ->ID );
322+ $ parent_title = apply_filters ( 'the_title ' , $ child ->post_title , $ child ->ID );
323+
324+ // level=2 → grandchild page; immediate parent is the child page.
325+ ob_start ();
326+ $ this ->table ->single_row ( $ grandchild , 2 );
327+ $ output = ob_get_clean ();
328+
329+ $ decoded = html_entity_decode ( $ output , ENT_QUOTES | ENT_HTML5 );
330+
331+ $ this ->assertStringContainsString ( 'aria-label= ' , $ output );
332+ $ this ->assertStringContainsString ( 'subpage of ' , $ decoded );
333+ $ this ->assertStringContainsString ( $ grandchild_title , $ decoded );
334+ $ this ->assertStringContainsString ( $ parent_title , $ decoded );
335+ $ this ->assertStringContainsString ( '(Edit) ' , $ decoded );
336+
337+ wp_set_current_user ( 0 );
338+ }
339+
340+ /**
341+ * Tests that a non-editable page (trashed) does not receive an aria-label on the title span.
342+ *
343+ * @ticket 62006
344+ *
345+ * @covers WP_Posts_List_Table::column_title
346+ */
347+ public function test_trashed_page_title_has_no_aria_label () {
348+ $ admin_id = self ::factory ()->user ->create ( array ( 'role ' => 'administrator ' ) );
349+ wp_set_current_user ( $ admin_id );
350+
351+ $ trashed = self ::factory ()->post ->create_and_get (
352+ array (
353+ 'post_type ' => 'page ' ,
354+ 'post_status ' => 'trash ' ,
355+ 'post_parent ' => self ::$ top [1 ]->ID ,
356+ )
357+ );
358+
359+ ob_start ();
360+ $ this ->table ->single_row ( $ trashed , 1 );
361+ $ output = ob_get_clean ();
362+
363+ // Trashed posts render as <span>, not <a class="row-title">, so no aria-label on the title.
364+ $ this ->assertStringNotContainsString ( 'class="row-title" ' , $ output );
365+ $ this ->assertStringNotContainsString ( 'aria-label="“ ' , $ output );
366+
367+ wp_set_current_user ( 0 );
368+ }
369+
248370 /**
249371 * @ticket 37407
250372 *
0 commit comments