From 9a33b0af4dc11e6d2654258cd1ae8838eda142f3 Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:37:10 -0600 Subject: [PATCH 1/7] Add distinct aria-label values for duplicate form landmarks The Styles edit page renders two
elements (style settings, live preview) and the Import/Export page renders two more (Import, Export) - all four shared no accessible name, tripping the IBM Equal Access aria_landmark_name_unique rule. Give each a distinct aria-label. Closes Strategy11/formidable-pro#6695 --- classes/helpers/FrmStylesPreviewHelper.php | 17 +++++++++ classes/views/styles/_styles-edit.php | 2 +- classes/views/xml/import_form.php | 4 +-- .../styles/test_FrmStylesController.php | 35 +++++++++++++++++++ tests/phpunit/xml/test_FrmXMLController.php | 25 +++++++++++++ 5 files changed, 80 insertions(+), 3 deletions(-) diff --git a/classes/helpers/FrmStylesPreviewHelper.php b/classes/helpers/FrmStylesPreviewHelper.php index 7ff39befcd..a9230d122c 100644 --- a/classes/helpers/FrmStylesPreviewHelper.php +++ b/classes/helpers/FrmStylesPreviewHelper.php @@ -208,6 +208,10 @@ public function get_html_for_form_preview() { // Force is_admin to false so the "Entry Key" field doesn't render in the preview. add_filter( 'frm_is_admin', '__return_false' ); + // The styler edit page also renders its own settings form, so this preview's landmark + // needs a distinct name to avoid tripping the aria_landmark_name_unique a11y rule. + add_filter( 'frm_form_attributes', array( $this, 'add_preview_landmark_label' ) ); + $target_form_preview_html = FrmFormsController::show_form( $this->form_id, '', 'auto', 'auto' ); $this->form_includes_captcha = wp_script_is( 'captcha-api', 'enqueued' ); @@ -217,6 +221,8 @@ public function get_html_for_form_preview() { wp_dequeue_script( 'captcha-api' ); } + remove_filter( 'frm_form_attributes', array( $this, 'add_preview_landmark_label' ) ); + // Return the is_admin status. // Otherwise success messages won't use the proper mark up and will appear without the green background and padding. remove_filter( 'frm_is_admin', '__return_false' ); @@ -224,6 +230,17 @@ public function get_html_for_form_preview() { return $target_form_preview_html; } + /** + * @since x.x + * + * @param string $attributes + * + * @return string + */ + public function add_preview_landmark_label( $attributes ) { + return $attributes . ' aria-label="' . esc_attr__( 'Form preview', 'formidable' ) . '"'; + } + /** * @since 6.0 * diff --git a/classes/views/styles/_styles-edit.php b/classes/views/styles/_styles-edit.php index 5dbd016d66..96a4084092 100644 --- a/classes/views/styles/_styles-edit.php +++ b/classes/views/styles/_styles-edit.php @@ -7,7 +7,7 @@ // It is accessed from /wp-admin/admin.php?page=formidable-styles&frm_action=edit&form=782 ?>
- + diff --git a/classes/views/xml/import_form.php b/classes/views/xml/import_form.php index e0702edc58..30d0ed4257 100644 --- a/classes/views/xml/import_form.php +++ b/classes/views/xml/import_form.php @@ -30,7 +30,7 @@ ?>


- +

@@ -71,7 +71,7 @@

- + diff --git a/tests/phpunit/styles/test_FrmStylesController.php b/tests/phpunit/styles/test_FrmStylesController.php index 0224aee62b..8f8212f315 100644 --- a/tests/phpunit/styles/test_FrmStylesController.php +++ b/tests/phpunit/styles/test_FrmStylesController.php @@ -94,6 +94,41 @@ public function test_render_style_page_has_no_duplicate_ids() { ); } + /** + * The styler edit view renders two elements on the same page: the style + * settings sidebar form, and the live form preview. Both need distinct + * accessible names or they violate the aria_landmark_name_unique a11y rule. + * + * @covers FrmStylesController::render_style_page + */ + public function test_render_style_page_has_unique_landmark_names_for_both_forms() { + $this->set_current_user_to_1(); + + $form_id = $this->factory->form->create(); + $form = FrmForm::getOne( $form_id ); + $frm_style = new FrmStyle( 'default' ); + $active_style = $frm_style->get_one(); + + ob_start(); + $this->run_private_method( + array( 'FrmStylesController', 'render_style_page' ), + array( $active_style, $form, $active_style ) + ); + $html = ob_get_clean(); + + preg_match_all( '/]*>/', $html, $matches ); + $this->assertCount( 2, $matches[0], 'Expected exactly two elements on the styler edit page' ); + + $labels = array(); + foreach ( $matches[0] as $form_tag ) { + preg_match( '/aria-label="([^"]*)"/', $form_tag, $label_match ); + $labels[] = $label_match[1] ?? ''; + } + + $this->assertNotContains( '', $labels, 'Every form landmark needs a non-empty accessible name' ); + $this->assertSame( array_unique( $labels ), $labels, 'Form landmarks must have distinct accessible names' ); + } + /** * @covers FrmStylesController::save_style * @covers FrmStyle::update diff --git a/tests/phpunit/xml/test_FrmXMLController.php b/tests/phpunit/xml/test_FrmXMLController.php index dff62ba233..b729209b78 100644 --- a/tests/phpunit/xml/test_FrmXMLController.php +++ b/tests/phpunit/xml/test_FrmXMLController.php @@ -23,4 +23,29 @@ public function test_validate_xml_url() { private function validate_xml_url( $url ) { return $this->run_private_method( array( 'FrmXMLController', 'validate_xml_url' ), array( $url ) ); } + + /** + * The Import/Export page renders two elements (Import, Export). Both + * need distinct accessible names or they violate the aria_landmark_name_unique + * a11y rule. + * + * @covers FrmXMLController::form + */ + public function test_form_has_unique_landmark_names_for_import_and_export_forms() { + ob_start(); + FrmXMLController::form(); + $html = ob_get_clean(); + + preg_match_all( '/]*>/', $html, $matches ); + $this->assertCount( 2, $matches[0], 'Expected exactly two elements on the Import/Export page' ); + + $labels = array(); + foreach ( $matches[0] as $form_tag ) { + preg_match( '/aria-label="([^"]*)"/', $form_tag, $label_match ); + $labels[] = $label_match[1] ?? ''; + } + + $this->assertNotContains( '', $labels, 'Every form landmark needs a non-empty accessible name' ); + $this->assertSame( array_unique( $labels ), $labels, 'Form landmarks must have distinct accessible names' ); + } } From 73ea606d860fc45651ceb75e714b39fa85df1ce8 Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:42:41 -0600 Subject: [PATCH 2/7] Extract shared unique-landmark-name assertion into FrmUnitTest Both new tests asserted the same thing (distinct, non-empty aria-label per ) with duplicated regex/assertion logic. --- tests/phpunit/base/FrmUnitTest.php | 27 +++++++++++++++++++ .../styles/test_FrmStylesController.php | 12 +-------- tests/phpunit/xml/test_FrmXMLController.php | 12 +-------- 3 files changed, 29 insertions(+), 22 deletions(-) diff --git a/tests/phpunit/base/FrmUnitTest.php b/tests/phpunit/base/FrmUnitTest.php index 4cf62b0cab..443d365081 100644 --- a/tests/phpunit/base/FrmUnitTest.php +++ b/tests/phpunit/base/FrmUnitTest.php @@ -862,4 +862,31 @@ protected function assert_no_duplicate_element_ids( $html, $ids ) { $this->assertSame( 1, $count, 'Expected exactly one element with id "' . $id . '"' ); } } + + /** + * Assert that every tag in some rendered HTML has a non-empty aria-label, + * and that no two forms share the same one (aria_landmark_name_unique). + * + * @since x.x + * + * @param string $html + * @param int $expected_count Required so an empty/short match list fails loudly instead of + * passing vacuously (assertNotContains/assertSame both pass on an + * empty array). + * + * @return void + */ + protected function assert_form_landmarks_have_unique_names( $html, $expected_count ) { + preg_match_all( '/]*>/', $html, $matches ); + $this->assertCount( $expected_count, $matches[0], 'Unexpected number of elements' ); + + $labels = array(); + foreach ( $matches[0] as $form_tag ) { + preg_match( '/aria-label="([^"]*)"/', $form_tag, $label_match ); + $labels[] = $label_match[1] ?? ''; + } + + $this->assertNotContains( '', $labels, 'Every form landmark needs a non-empty accessible name' ); + $this->assertSame( array_unique( $labels ), $labels, 'Form landmarks must have distinct accessible names' ); + } } diff --git a/tests/phpunit/styles/test_FrmStylesController.php b/tests/phpunit/styles/test_FrmStylesController.php index 8f8212f315..71a1266633 100644 --- a/tests/phpunit/styles/test_FrmStylesController.php +++ b/tests/phpunit/styles/test_FrmStylesController.php @@ -116,17 +116,7 @@ public function test_render_style_page_has_unique_landmark_names_for_both_forms( ); $html = ob_get_clean(); - preg_match_all( '/]*>/', $html, $matches ); - $this->assertCount( 2, $matches[0], 'Expected exactly two elements on the styler edit page' ); - - $labels = array(); - foreach ( $matches[0] as $form_tag ) { - preg_match( '/aria-label="([^"]*)"/', $form_tag, $label_match ); - $labels[] = $label_match[1] ?? ''; - } - - $this->assertNotContains( '', $labels, 'Every form landmark needs a non-empty accessible name' ); - $this->assertSame( array_unique( $labels ), $labels, 'Form landmarks must have distinct accessible names' ); + $this->assert_form_landmarks_have_unique_names( $html, 2 ); } /** diff --git a/tests/phpunit/xml/test_FrmXMLController.php b/tests/phpunit/xml/test_FrmXMLController.php index b729209b78..87dd91e066 100644 --- a/tests/phpunit/xml/test_FrmXMLController.php +++ b/tests/phpunit/xml/test_FrmXMLController.php @@ -36,16 +36,6 @@ public function test_form_has_unique_landmark_names_for_import_and_export_forms( FrmXMLController::form(); $html = ob_get_clean(); - preg_match_all( '/]*>/', $html, $matches ); - $this->assertCount( 2, $matches[0], 'Expected exactly two elements on the Import/Export page' ); - - $labels = array(); - foreach ( $matches[0] as $form_tag ) { - preg_match( '/aria-label="([^"]*)"/', $form_tag, $label_match ); - $labels[] = $label_match[1] ?? ''; - } - - $this->assertNotContains( '', $labels, 'Every form landmark needs a non-empty accessible name' ); - $this->assertSame( array_unique( $labels ), $labels, 'Form landmarks must have distinct accessible names' ); + $this->assert_form_landmarks_have_unique_names( $html, 2 ); } } From a2975360ce0b4dee8dff4d80c42a2599f0fdbb25 Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:46:19 -0600 Subject: [PATCH 3/7] Fix PHP CS Fixer blank_line_before_statement before foreach --- tests/phpunit/base/FrmUnitTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/phpunit/base/FrmUnitTest.php b/tests/phpunit/base/FrmUnitTest.php index 443d365081..9fff267c67 100644 --- a/tests/phpunit/base/FrmUnitTest.php +++ b/tests/phpunit/base/FrmUnitTest.php @@ -881,6 +881,7 @@ protected function assert_form_landmarks_have_unique_names( $html, $expected_cou $this->assertCount( $expected_count, $matches[0], 'Unexpected number of elements' ); $labels = array(); + foreach ( $matches[0] as $form_tag ) { preg_match( '/aria-label="([^"]*)"/', $form_tag, $label_match ); $labels[] = $label_match[1] ?? ''; From 4ce1c9e01e0eaecca2f9513c46f42064176d1b56 Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2026 15:50:30 -0600 Subject: [PATCH 4/7] Pin $_GET in styles landmark test so suite order can't switch it to list view render_style_page() reads $_GET (form/style_id) to pick 'edit' vs 'list'; a leftover value from another test would silently render the list view's single form instead, making this fail for the wrong reason. --- tests/phpunit/styles/test_FrmStylesController.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/phpunit/styles/test_FrmStylesController.php b/tests/phpunit/styles/test_FrmStylesController.php index 71a1266633..697cea7f21 100644 --- a/tests/phpunit/styles/test_FrmStylesController.php +++ b/tests/phpunit/styles/test_FrmStylesController.php @@ -104,6 +104,10 @@ public function test_render_style_page_has_no_duplicate_ids() { public function test_render_style_page_has_unique_landmark_names_for_both_forms() { $this->set_current_user_to_1(); + // render_style_page() reads $_GET to decide the view ('edit' vs 'list'); a leftover + // 'form'/'style_id' from another test would silently switch this to the list view. + $_GET = array(); + $form_id = $this->factory->form->create(); $form = FrmForm::getOne( $form_id ); $frm_style = new FrmStyle( 'default' ); From 26b5b308084d2fa8109726c0c90aceab0e7eb3ba Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 08:15:40 -0600 Subject: [PATCH 5/7] Add distinct aria-label to the styler list view's assign-style form The list view (reached via a form/style_id param with no frm_action, e.g. FrmStylesHelper::get_list_url()'s live navigation links) rendered #frm_style_list_form with no accessible name alongside the preview form's "Form preview" label, violating aria_landmark_name_unique the same way the edit view did before this PR. admin-a11y.cy.js never exercises this view (it only visits the bare ?page=formidable-styles URL, which falls back to the edit view), so CI never caught it. --- classes/views/styles/_styles-list.php | 2 +- .../styles/test_FrmStylesController.php | 32 +++++++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/classes/views/styles/_styles-list.php b/classes/views/styles/_styles-list.php index 064a4ffc81..692051a629 100644 --- a/classes/views/styles/_styles-list.php +++ b/classes/views/styles/_styles-list.php @@ -44,7 +44,7 @@ - + diff --git a/tests/phpunit/styles/test_FrmStylesController.php b/tests/phpunit/styles/test_FrmStylesController.php index 697cea7f21..28bd647de7 100644 --- a/tests/phpunit/styles/test_FrmStylesController.php +++ b/tests/phpunit/styles/test_FrmStylesController.php @@ -123,6 +123,38 @@ public function test_render_style_page_has_unique_landmark_names_for_both_forms( $this->assert_form_landmarks_have_unique_names( $html, 2 ); } + /** + * The styler list view (reached whenever a 'form'/'style_id' param is present without + * 'frm_action') renders two elements as well: the style-assign form, and the live + * form preview. Both need distinct accessible names or they violate the + * aria_landmark_name_unique a11y rule the same way the edit view does above. + * + * @covers FrmStylesController::render_style_page + */ + public function test_render_style_page_has_unique_landmark_names_for_list_view() { + $this->set_current_user_to_1(); + + $form_id = $this->factory->form->create(); + + // A 'form' param with no 'frm_action' is what forces the list view (see comment above). + $_GET = array( 'form' => $form_id ); + + $form = FrmForm::getOne( $form_id ); + $frm_style = new FrmStyle( 'default' ); + $active_style = $frm_style->get_one(); + + ob_start(); + $this->run_private_method( + array( 'FrmStylesController', 'render_style_page' ), + array( $active_style, $form, $active_style ) + ); + $html = ob_get_clean(); + + $_GET = array(); + + $this->assert_form_landmarks_have_unique_names( $html, 2 ); + } + /** * @covers FrmStylesController::save_style * @covers FrmStyle::update From 2400436fdadda1c059911445619ecd483ef925be Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 08:15:44 -0600 Subject: [PATCH 6/7] Address non-blocking review notes: try/finally filter cleanup, less brittle XML landmark test get_html_for_form_preview() now removes its two filters in a finally block so a thrown exception from FrmFormsController::show_form() can't leave add_preview_landmark_label registered on frm_form_attributes for the rest of the request. test_form_has_unique_landmark_names_for_import_and_export_forms() now asserts the Import/Export labels directly instead of a bare form count, so an unrelated hook adding its own to that page (e.g. frm_import_settings) doesn't fail this test. --- classes/helpers/FrmStylesPreviewHelper.php | 26 +++++++++++---------- tests/phpunit/xml/test_FrmXMLController.php | 9 ++++++- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/classes/helpers/FrmStylesPreviewHelper.php b/classes/helpers/FrmStylesPreviewHelper.php index a9230d122c..6e0955a716 100644 --- a/classes/helpers/FrmStylesPreviewHelper.php +++ b/classes/helpers/FrmStylesPreviewHelper.php @@ -212,22 +212,24 @@ public function get_html_for_form_preview() { // needs a distinct name to avoid tripping the aria_landmark_name_unique a11y rule. add_filter( 'frm_form_attributes', array( $this, 'add_preview_landmark_label' ) ); - $target_form_preview_html = FrmFormsController::show_form( $this->form_id, '', 'auto', 'auto' ); + try { + $target_form_preview_html = FrmFormsController::show_form( $this->form_id, '', 'auto', 'auto' ); - $this->form_includes_captcha = wp_script_is( 'captcha-api', 'enqueued' ); + $this->form_includes_captcha = wp_script_is( 'captcha-api', 'enqueued' ); - if ( $this->form_includes_captcha ) { - // If a form includes a CAPTCHA field, don't try to load the CAPTCHA scripts for the visual styler preview. - wp_dequeue_script( 'captcha-api' ); - } - - remove_filter( 'frm_form_attributes', array( $this, 'add_preview_landmark_label' ) ); + if ( $this->form_includes_captcha ) { + // If a form includes a CAPTCHA field, don't try to load the CAPTCHA scripts for the visual styler preview. + wp_dequeue_script( 'captcha-api' ); + } - // Return the is_admin status. - // Otherwise success messages won't use the proper mark up and will appear without the green background and padding. - remove_filter( 'frm_is_admin', '__return_false' ); + return $target_form_preview_html; + } finally { + remove_filter( 'frm_form_attributes', array( $this, 'add_preview_landmark_label' ) ); - return $target_form_preview_html; + // Return the is_admin status. + // Otherwise success messages won't use the proper mark up and will appear without the green background and padding. + remove_filter( 'frm_is_admin', '__return_false' ); + } } /** diff --git a/tests/phpunit/xml/test_FrmXMLController.php b/tests/phpunit/xml/test_FrmXMLController.php index 87dd91e066..787310daea 100644 --- a/tests/phpunit/xml/test_FrmXMLController.php +++ b/tests/phpunit/xml/test_FrmXMLController.php @@ -36,6 +36,13 @@ public function test_form_has_unique_landmark_names_for_import_and_export_forms( FrmXMLController::form(); $html = ob_get_clean(); - $this->assert_form_landmarks_have_unique_names( $html, 2 ); + // Assert the two specific labels directly rather than a bare form count — a hook fired inside + // FrmXMLController::form() (e.g. frm_import_settings) adding its own is a real extension + // point other add-ons already use here, and shouldn't fail this test as long as Import/Export + // themselves still have distinct, non-empty accessible names. + preg_match_all( '/]*aria-label="([^"]*)"[^>]*>/', $html, $matches ); + $this->assertContains( 'Import', $matches[1], 'Import form is missing its aria-label' ); + $this->assertContains( 'Export', $matches[1], 'Export form is missing its aria-label' ); + $this->assertSame( array_unique( $matches[1] ), $matches[1], 'Form landmarks must have distinct accessible names' ); } } From 38ec34b1d903fe17fe7bfb0e2516dd9bd702eab2 Mon Sep 17 00:00:00 2001 From: "vivi-the-going-merry[bot]" <308115520+vivi-the-going-merry[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 08:57:04 -0600 Subject: [PATCH 7/7] Fix PHPCS: no blank line between consecutive assignments --- tests/phpunit/styles/test_FrmStylesController.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/phpunit/styles/test_FrmStylesController.php b/tests/phpunit/styles/test_FrmStylesController.php index 28bd647de7..0c9dbc484f 100644 --- a/tests/phpunit/styles/test_FrmStylesController.php +++ b/tests/phpunit/styles/test_FrmStylesController.php @@ -149,7 +149,6 @@ public function test_render_style_page_has_unique_landmark_names_for_list_view() array( $active_style, $form, $active_style ) ); $html = ob_get_clean(); - $_GET = array(); $this->assert_form_landmarks_have_unique_names( $html, 2 );