diff --git a/classes/views/shared/toggle.php b/classes/views/shared/toggle.php index a4afa12813..f40c53b95e 100644 --- a/classes/views/shared/toggle.php +++ b/classes/views/shared/toggle.php @@ -26,6 +26,7 @@ $use_container = false; $aria_attrs = array(); + if ( ! empty( $args['aria-label-attr'] ) ) { $aria_attrs['aria-label'] = $args['aria-label-attr']; } else { diff --git a/package-lock.json b/package-lock.json index cdc6a65880..3474058091 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8734,6 +8734,17 @@ } } }, + "node_modules/@wordpress/url/node_modules/@types/react": { + "version": "19.3.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz", + "integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, "node_modules/@wordpress/url/node_modules/commander": { "version": "12.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", diff --git a/tests/cypress/e2e/Entries/EntriesPageDataValidations.cy.js b/tests/cypress/e2e/Entries/EntriesPageDataValidations.cy.js index 134c5f4ffd..5aa3824d95 100644 --- a/tests/cypress/e2e/Entries/EntriesPageDataValidations.cy.js +++ b/tests/cypress/e2e/Entries/EntriesPageDataValidations.cy.js @@ -14,7 +14,8 @@ describe( 'Entries submitted from a form', () => { cy.get( '#frm-save-form-name-button' ).should( 'contain', 'Save' ).click(); cy.log( `Create a text field` ); - cy.get( `li[id="text"] a[title="Text"]` ).click( { force: true } ); + // Plain, always-visible sidebar link - no hover gating involved. + cy.get( `li[id="text"] a[title="Text"]` ).should( 'be.visible' ).click(); cy.log( 'Update form' ); cy.get( '#frm_submit_side_top' ).should( 'contain', 'Update' ).click(); @@ -74,7 +75,7 @@ describe( 'Entries submitted from a form', () => { } ); cy.get( '.frm_form_nav > :nth-child(1) > a' ).should( 'contain', 'Build' ).click(); - cy.get( "a[aria-label='Close']", { timeout: 5000 } ).click( { force: true } ); + cy.get( "a[aria-label='Close']", { timeout: 5000 } ).should( 'be.visible' ).click(); cy.log( 'Verify that entries are not allowed from the forms list' ); cy.get( 'td[data-colname="Entries"] svg[title="Saving entries is disabled for this form"]' ).should( 'exist' ); } ); @@ -88,7 +89,8 @@ describe( 'Entries submitted from a form', () => { cy.get( '#frm-save-form-name-button' ).should( 'contain', 'Save' ).click(); cy.log( `Create a text field` ); - cy.get( `li[id="text"] a[title="Text"]` ).click( { force: true } ); + // Plain, always-visible sidebar link - no hover gating involved. + cy.get( `li[id="text"] a[title="Text"]` ).should( 'be.visible' ).click(); cy.log( 'Update form' ); cy.get( '#frm_submit_side_top' ).should( 'contain', 'Update' ).click(); @@ -130,9 +132,15 @@ describe( 'Entries submitted from a form', () => { cy.get( 'td[data-colname="IP"]' ).should( 'exist' ); cy.log( 'Click on View' ); - cy.get( 'tr div.row-actions span.view a', { timeout: 5000 } ) + // WP core only reveals row-actions on a real CSS `:hover` (`.row-actions` is + // `position: relative; left: -9999em` until `tr:hover`) - make it actionable the way the + // real hover would, then click normally. Chained in one continuous command so there's no + // window between the reset and the click for a re-render to undo it. + cy.get( 'tr div.row-actions', { timeout: 5000 } ) + .invoke( 'css', 'position', 'static' ) + .find( 'span.view a' ) .should( 'contain', 'View' ) - .click( { force: true } ); + .click(); cy.url().should( 'include', 'frm_action=show&id=' ); @@ -188,7 +196,7 @@ describe( 'Entries submitted from a form', () => { cy.get( '.frm_no_entries_header' ).should( 'contain', 'No Entries for form: Test Form' ); cy.get( '.frm_form_nav > :nth-child(1) > a' ).should( 'contain', 'Build' ).click(); - cy.get( "a[aria-label='Close']", { timeout: 5000 } ).click( { force: true } ); + cy.get( "a[aria-label='Close']", { timeout: 5000 } ).should( 'be.visible' ).click(); } ); afterEach( () => { diff --git a/tests/cypress/e2e/Entries/deleteEntries.cy.js b/tests/cypress/e2e/Entries/deleteEntries.cy.js index 30d5ed08fc..bf61f7150c 100644 --- a/tests/cypress/e2e/Entries/deleteEntries.cy.js +++ b/tests/cypress/e2e/Entries/deleteEntries.cy.js @@ -14,11 +14,16 @@ describe( 'Entries submitted from a form', () => { cy.get( '#frm-save-form-name-button' ).should( 'contain', 'Save' ).click(); cy.log( `Add some fields` ); - cy.get( 'li[id="text"] a[title="Text"]' ).click( { force: true } ); - cy.get( 'li[id="name"] a[title="Name"]' ).click( { force: true } ); - cy.get( 'li[id="checkbox"] a[title="Checkboxes"]' ).click( { force: true } ); - cy.get( 'li[id="email"] a[title="Email"]' ).click( { force: true } ); - cy.get( 'li[id="phone"] a[title="Phone"]' ).click( { force: true } ); + // Plain, always-visible sidebar links - no hover gating involved. Adding a field can leave + // the scrollable field-type panel (.frm-right-panel) scrolled to wherever the previous click + // left it, so a later link in the list can sit outside its own panel's visible scroll area - + // not a render-timing race (a longer timeout never resolves it), so scroll each one into view + // within its own panel before asserting visible. + cy.get( 'li[id="text"] a[title="Text"]' ).scrollIntoView().should( 'be.visible' ).click(); + cy.get( 'li[id="name"] a[title="Name"]' ).scrollIntoView().should( 'be.visible' ).click(); + cy.get( 'li[id="checkbox"] a[title="Checkboxes"]' ).scrollIntoView().should( 'be.visible' ).click(); + cy.get( 'li[id="email"] a[title="Email"]' ).scrollIntoView().should( 'be.visible' ).click(); + cy.get( 'li[id="phone"] a[title="Phone"]' ).scrollIntoView().should( 'be.visible' ).click(); cy.log( 'Update form' ); cy.get( '#frm_submit_side_top' ).should( 'contain', 'Update' ).click(); @@ -55,7 +60,15 @@ describe( 'Entries submitted from a form', () => { cy.go( 'back' ); cy.contains( '#the-list tr', 'Entry test' ).trigger( 'mouseover' ).then( $row => { cy.wrap( $row ).within( () => { - cy.get( '.row-actions .delete .submitdelete' ).should( 'be.visible' ).click( { force: true } ); + // WP core only reveals row-actions on a real CSS `:hover` (`.row-actions` is + // `position: relative; left: -9999em` until `tr:hover`) - make it actionable the + // way the real hover would, then click normally. Chained in one continuous command + // so there's no window between the reset and the click for a re-render to undo it. + cy.get( '.row-actions' ) + .invoke( 'css', 'position', 'static' ) + .find( '.delete .submitdelete' ) + .should( 'be.visible' ) + .click(); } ); } ); cy.contains( '.frm-confirm-msg', 'Permanently delete this entry?' ); @@ -63,7 +76,7 @@ describe( 'Entries submitted from a form', () => { cy.log( 'Teardown - Close and delete form' ); cy.get( '.frm_form_nav > :nth-child(1) > a' ).should( 'contain', 'Build' ).click(); - cy.get( "a[aria-label='Close']", { timeout: 5000 } ).click( { force: true } ); + cy.get( "a[aria-label='Close']", { timeout: 5000 } ).should( 'be.visible' ).click(); cy.deleteForm(); } ); } ); diff --git a/tests/cypress/e2e/Form Templates/FormTemplates.cy.js b/tests/cypress/e2e/Form Templates/FormTemplates.cy.js index c49f8d1f89..1979e38b82 100644 --- a/tests/cypress/e2e/Form Templates/FormTemplates.cy.js +++ b/tests/cypress/e2e/Form Templates/FormTemplates.cy.js @@ -261,11 +261,16 @@ describe( 'Form Templates page', () => { it( 'add templates as favorites, view demo and use templates', () => { cy.log( 'Add contact us template as favorite' ); + // Verified via Playwright: `.frm-form-templates-item-favorite-button` is `display: none` + // until the card is hovered (a real CSS `:hover`, which Cypress can't simulate before its + // own pre-click check) - make it actionable the way the real hover would, then click + // normally. cy.contains( 'li', 'Contact Us', { timeout: 10000 } ) .first() - .trigger( 'mouseover', { force: true } ) + .should( 'be.visible' ) .find( '.frm-form-templates-item-favorite-button' ) - .click( { force: true } ); + .invoke( 'css', 'display', 'flex' ) + .click(); cy.get( '[data-category="favorites"] > .frm-page-skeleton-cat-count' ).should( 'contain.text', '1' ); cy.get( '[data-category="favorites"]' ).click(); @@ -278,13 +283,20 @@ describe( 'Form Templates page', () => { cy.get( '[data-category="all-items"]' ).should( 'contain', 'All Templates' ).click(); cy.log( 'View demo of the contact us template' ); + // Verified via Playwright: the button row (`.frm-form-templates-item-buttons`) is + // `display: none` until the card is hovered - make it actionable the way the real hover + // would, then click normally. + cy.contains( 'li', 'Contact Us', { timeout: 10000 } ) + .first() + .should( 'be.visible' ) + .find( '.frm-form-templates-item-buttons' ) + .invoke( 'css', 'display', 'flex' ); cy.contains( 'li', 'Contact Us', { timeout: 10000 } ) .first() - .trigger( 'mouseover', { force: true } ) .find( '.frm-button-secondary' ) .invoke( 'removeAttr', 'target' ) .should( 'contain', 'View Demo' ) - .click( { force: true } ); + .click(); cy.origin( 'https://formidableforms.com', () => { cy.get( 'h1' ).should( 'have.text', 'Contact Us Form Template' ); @@ -292,33 +304,38 @@ describe( 'Form Templates page', () => { cy.visit( '/wp-admin/admin.php?page=formidable-form-templates' ); - cy.contains( 'li', 'Contact Us', { timeout: 10000 } ) - .first() - .trigger( 'mouseover', { force: true } ) - .find( '.frm-form-templates-use-template-button' ) - .should( 'contain', 'Use Template' ); - // cy.log( 'Try to use available templates' ); // cy.get( '[data-category="available-templates"]' ).should( 'contain', 'Available Templates' ).click(); // cy.get( '#frm-form-templates-page-title-text' ).should( 'contain', 'Available Templates' ); + // Invoke display:flex and click in one continuous chain, right before the click - a + // separate cy.contains() re-query for the click (as this used to do) leaves a window where + // a React re-render of this list can restore its own controlled style and re-hide the + // button before the click actually lands. cy.contains( 'li', 'Contact Us', { timeout: 10000 } ) .first() - .trigger( 'mouseover', { force: true } ) + .should( 'be.visible' ) + .find( '.frm-form-templates-item-buttons' ) + .invoke( 'css', 'display', 'flex' ) .find( '.frm-form-templates-use-template-button' ) .should( 'contain', 'Use Template' ) - .click( { force: true } ); + .click(); - cy.get( 'svg[aria-label="Close"]' ).click( { force: true } ); + cy.get( 'svg[aria-label="Close"]' ).should( 'be.visible' ).click(); cy.visit( '/wp-admin/admin.php?page=formidable-form-templates' ); + // Same real CSS `:hover` reveal on the template card as above - make it actionable the + // way the real hover would, then click normally. + cy.get( '[frm-search-text="user registration"]' ) + .first() + .find( '.frm-form-templates-item-buttons' ) + .invoke( 'css', 'display', 'flex' ); cy.get( '[frm-search-text="user registration"]' ) .first() - .trigger( 'mouseover' ) .find( '.frm-form-templates-use-template-button' ) .should( 'contain', 'Use Template' ) - .click( { force: true } ); + .click(); cy.get( '#frm-form-upgrade-modal > .frm_modal_top > .frm-modal-title > h2' ).should( 'contain', 'User Registration is a PRO Template' ); cy.get( '#frm-form-upgrade-modal > .inside > :nth-child(1)' ).should( 'contain', 'The User Registration is not available on your plan. Please upgrade to unlock this and more awesome templates.' ); @@ -331,12 +348,14 @@ describe( 'Form Templates page', () => { } ); it( 'create a new custom template and delete it', () => { + // Plain, always-visible button - no force needed (an identical unforced click on this + // selector works in commands.js's createNewForm()); Cypress auto-scrolls into view. cy.get( '#frm-form-templates-create-form' ) .should( 'contain', 'Create a blank form' ) - .scrollIntoView() - .click( { force: true } ); + .click(); - cy.get( '#frm_submit_side_top' ).should( 'contain', 'Save' ).click( { force: true } ); + // Plain #frm_submit_side_top "Save" click - no force needed, see the note above. + cy.get( '#frm_submit_side_top' ).should( 'contain', 'Save' ).click(); cy.log( 'Ensure the modal for saving form is visible' ); cy.get( '#frm-form-templates-modal' ).should( 'be.visible' ); @@ -355,10 +374,11 @@ describe( 'Form Templates page', () => { .should( 'contain.text', 'Cancel' ); cy.get( '#frm_new_form_name_input' ).type( 'Form Template Test' ); - cy.get( '#frm-save-form-name-button' ).should( 'contain', 'Save' ).click( { force: true } ); + // Plain #frm-save-form-name-button click - no force needed (see commands.js's createNewForm()). + cy.get( '#frm-save-form-name-button' ).should( 'contain', 'Save' ).click(); cy.get( "a[aria-label='Close'] svg", { timeout: 10000 } ) .should( 'be.visible' ) - .click( { force: true } ); + .click(); cy.get( '#toplevel_page_formidable > .wp-submenu > :nth-child(8) > a' ).should( 'contain', 'Form Templates' ).click(); cy.get( '[data-category="custom"]' ).click(); @@ -366,20 +386,20 @@ describe( 'Form Templates page', () => { cy.log( 'Validate that there are no custom templates yet' ); cy.get( '.frmcenter > .frm-page-skeleton-title' ).should( 'contain', 'You currently have no templates.' ); cy.get( '.frm-page-skeleton-text' ).should( 'contain', "You haven't created any form templates. Begin now to simplify your workflow and save time." ); - cy.get( '#frm-page-skeleton-empty-state > .button' ).should( 'contain', 'Create Template' ).click( { force: true } ); + cy.get( '#frm-page-skeleton-empty-state > .button' ).should( 'be.visible' ).and( 'contain', 'Create Template' ).click(); cy.get( '#frm-create-template-modal > .frm_modal_footer > .button-secondary' ).should( 'contain', 'Cancel' ).click(); cy.log( 'Create a new template' ); - cy.get( '#frm-page-skeleton-empty-state > .button' ).should( 'contain', 'Create Template' ).click( { force: true } ); + cy.get( '#frm-page-skeleton-empty-state > .button' ).should( 'be.visible' ).and( 'contain', 'Create Template' ).click(); cy.get( '#frm-create-template-modal > .frm_modal_top > .frm-modal-title > h2' ).should( 'contain', 'Create New Template' ); cy.get( '.inside > :nth-child(1) > label' ).should( 'contain', 'Select form for a new template' ); cy.get( '#frm-create-template-modal-forms-select' ).select( 'Form Template Test' ); cy.get( ':nth-child(3) > label' ).should( 'contain', 'Description' ); cy.get( '#frm_create_template_description' ).type( 'Test description' ); - cy.get( '#frm-create-template-button' ).should( 'contain', 'Create Template' ).click( { force: true } ); + cy.get( '#frm-create-template-button' ).should( 'be.visible' ).and( 'contain', 'Create Template' ).click(); cy.get( "a[aria-label='Close'] svg", { timeout: 10000 } ) .should( 'be.visible' ) - .click( { force: true } ); + .click(); cy.get( '.row-title' ).should( 'contain', 'Form Template Test Template' ); cy.get( '#toplevel_page_formidable > .wp-submenu > :nth-child(8) > a' ).should( 'contain', 'Form Templates' ).click(); @@ -392,43 +412,52 @@ describe( 'Form Templates page', () => { .should( 'contain', 'Test description' ); cy.log( 'Edit template' ); + // Same button-row `display: none` -> hover reveal as above - make it actionable the way + // the real hover would, then click normally. + cy.get( 'li[frm-search-text="form template test template"]' ) + .find( '.frm-form-templates-item-buttons' ) + .invoke( 'css', 'display', 'flex' ); cy.get( 'li[frm-search-text="form template test template"]' ) - .trigger( 'mouseover' ) .find( '.frm-button-secondary' ) .should( 'contain', 'Edit' ) - .click( { force: true } ); + .click(); cy.get( "a[aria-label='Close'] svg", { timeout: 10000 } ) .should( 'be.visible' ) - .click( { force: true } ); + .click(); cy.get( '#toplevel_page_formidable > .wp-submenu > :nth-child(8) > a' ).should( 'contain', 'Form Templates' ).click(); cy.get( '[data-category="custom"]' ).click(); cy.log( 'Click on the use template button' ); + // Same button-row `display: none` -> hover reveal as above. + cy.get( 'li[frm-search-text="form template test template"]' ) + .find( '.frm-form-templates-item-buttons' ) + .invoke( 'css', 'display', 'flex' ); cy.get( 'li[frm-search-text="form template test template"]' ) - .trigger( 'mouseover' ) .find( '.frm-button-primary' ) .should( 'contain', 'Use Template' ) - .click( { force: true } ); + .click(); cy.get( "a[aria-label='Close'] svg", { timeout: 10000 } ) .should( 'be.visible' ) - .click( { force: true } ); + .click(); cy.get( '#toplevel_page_formidable > .wp-submenu > :nth-child(8) > a' ).should( 'contain', 'Form Templates' ).click(); cy.get( '[data-category="custom"]' ).click(); cy.log( 'Delete template' ); + // This button is `display: none` until the card is hovered - make it actionable the way + // the real hover would, then click normally. cy.get( 'li[frm-search-text="form template test template"]' ) - .trigger( 'mouseover' ) .find( '.frm-form-templates-custom-item-trash-button' ) - .click( { force: true } ); + .invoke( 'css', 'display', 'flex' ) + .click(); cy.get( '.cta-inside > .frm-flex-box > .button-secondary' ).should( 'contain', 'Cancel' ).click(); cy.get( 'li[frm-search-text="form template test template"]' ) - .trigger( 'mouseover' ) .find( '.frm-form-templates-custom-item-trash-button' ) - .click( { force: true } ); + .invoke( 'css', 'display', 'flex' ) + .click(); cy.get( '.frm-confirm-msg' ).should( 'contain', 'Do you want to move this form template to the trash?' ); cy.get( '#frm-confirmed-click' ).should( 'contain', 'Confirm' ).click(); @@ -442,11 +471,14 @@ describe( 'Form Templates page', () => { cy.log( 'Get free templates by clicking Use Template' ); cy.get( '[data-category="all-items"]' ).should( 'contain', 'All Templates' ).click(); cy.get( '#frm-form-templates-page-title-text' ).should( 'contain', 'All Templates' ); + // Same button-row `display: none` -> hover reveal as above. + cy.get( '[frm-search-text="grade book"]' ) + .find( '.frm-form-templates-item-buttons' ) + .invoke( 'css', 'display', 'flex' ); cy.get( '[frm-search-text="grade book"]' ) - .trigger( 'mouseover' ) .find( '.frm-form-templates-use-template-button' ) .should( 'contain', 'Use Template' ) - .click( { force: true } ); + .click(); cy.get( '#frm-leave-email-modal' ).should( 'be.visible' ); cy.get( '#frm-leave-email-modal > .frm_modal_top > .frm-modal-title > h2' ).should( 'contain', 'Get 30+ Free Form Templates' ); @@ -458,10 +490,12 @@ describe( 'Form Templates page', () => { cy.get( '#frm-leave-email-modal > .frm_modal_footer > .button-secondary' ).click(); cy.get( '[frm-search-text="grade book"]' ) - .trigger( 'mouseover' ) + .find( '.frm-form-templates-item-buttons' ) + .invoke( 'css', 'display', 'flex' ); + cy.get( '[frm-search-text="grade book"]' ) .find( '.frm-form-templates-use-template-button' ) .should( 'contain', 'Use Template' ) - .click( { force: true } ); + .click(); cy.get( 'a#frm-get-code-button' ).click(); @@ -470,10 +504,13 @@ describe( 'Form Templates page', () => { cy.log( 'Try to use free templates' ); cy.get( '[frm-search-text="employee referral"]' ) .first() - .trigger( 'mouseover' ) + .find( '.frm-form-templates-item-buttons' ) + .invoke( 'css', 'display', 'flex' ); + cy.get( '[frm-search-text="employee referral"]' ) + .first() .find( '.frm-form-templates-use-template-button' ) .should( 'contain', 'Use Template' ) - .click( { force: true } ); + .click(); cy.get( 'span[title]' ).should( 'contain', 'Employee Referral' ); cy.get( "a[aria-label='Close']", { timeout: 5000 } ).click(); @@ -481,12 +518,21 @@ describe( 'Form Templates page', () => { cy.contains( '#the-list tr', 'Employee Referral' ).trigger( 'mouseover' ).then( $row => { console.log( 'Hovered Row:', $row ); cy.wrap( $row ).within( () => { - cy.get( '.row-actions .trash .frm-trash-link' ).should( 'be.visible' ).click( { force: true } ); + // WP hides row-actions until a real CSS `:hover` - make it actionable the way the real + // hover would, then click normally. Chained in one continuous command so there's no + // window between the reset and the click for a re-render to undo it. + cy.get( '.row-actions' ) + .invoke( 'css', 'position', 'static' ) + .find( '.trash .frm-trash-link' ) + .should( 'be.visible' ) + .click(); } ); cy.get( 'body' ).then( $body => { if ( $body.find( "div[role='dialog']" ).length ) { cy.get( "div[role='dialog']" ).should( 'be.visible' ).and( 'contain.text', 'Do you want to move this form to the trash?' ); - cy.xpath( "//a[@id='frm-confirmed-click']" ).should( 'contain.text', 'Confirm' ).click( { force: true } ); + // Plain cy.get() by id rather than cy.xpath() - the xpath-resolved element doesn't + // re-query the same way on Cypress's retry; plain cy.get() works unforced elsewhere. + cy.get( '#frm-confirmed-click' ).should( 'contain.text', 'Confirm' ).click(); } else { cy.log( 'Dialog not found' ); } diff --git a/tests/cypress/e2e/Forms/deleteForms.cy.js b/tests/cypress/e2e/Forms/deleteForms.cy.js index dbb1ced6a6..84e5029f2b 100644 --- a/tests/cypress/e2e/Forms/deleteForms.cy.js +++ b/tests/cypress/e2e/Forms/deleteForms.cy.js @@ -5,20 +5,34 @@ describe( 'Deleting forms', () => { cy.viewport( 1280, 720 ); } ); + // Select only the "Test Form" rows on whatever list table is currently showing, rather than + // the header "select all" checkbox - the forms list isn't necessarily empty going into this + // test (e.g. the "Contact Us" form Formidable creates by default on a fresh install), and a + // blanket "select all" would sweep that up into this test's own trash/delete/restore cycle. + const checkTestFormRows = () => { + cy.get( 'tr' ).filter( ( index, element ) => { + return Cypress.$( element ).text().includes( 'Test Form' ); + } ).each( $row => { + cy.wrap( $row ).find( '.check-column input[type="checkbox"]' ).check(); + } ); + }; + it( 'should create multiple forms and bulk delete them', () => { cy.emptyTrash(); + cy.log( 'Record the published form count so later assertions allow for forms this test did not create, like the default Contact Us form' ); + let baselinePublishedCount = '(0)'; + cy.get( '.published > a .count' ).invoke( 'text' ).then( text => { + baselinePublishedCount = text; + } ); + cy.log( 'Create 5 new forms' ); for ( let i = 0; i < 5; i++ ) { cy.createNewForm(); } cy.log( 'Bulk delete all 5 new forms' ); - cy.get( 'tr' ).filter( ( index, element ) => { - return Cypress.$( element ).text().includes( 'Test Form' ); - } ).each( $row => { - cy.wrap( $row ).find( '.check-column input[type="checkbox"]' ).check(); - } ); + checkTestFormRows(); cy.log( 'Click on Bulk Actions and select the Move to Trash option' ); cy.get( '#bulk-action-selector-top' ).select( 'Move to Trash' ); @@ -36,11 +50,8 @@ describe( 'Deleting forms', () => { cy.get( '.colspanchange > p' ).should( 'contain', 'No forms found in the trash.' ); cy.get( '.colspanchange > p > a' ).should( 'contain', 'See all forms' ).click(); cy.log( 'Bulk delete permanently deleted forms' ); - cy.get( '#bulk-action-selector-top' ).select( 'Move to Trash' ); - cy.get( '#doaction' ).should( 'contain', 'Apply' ).click(); - cy.get( '.trash > a' ).should( 'contain.text', 'Trash' ); - cy.get( '#cb-select-all-1' ).click(); - cy.get( '#bulk-action-selector-top' ).should( 'contain', 'Bulk Actions' ).select( 'bulk_trash' ); + checkTestFormRows(); + cy.get( '#bulk-action-selector-top' ).select( 'bulk_trash' ); cy.get( '#doaction' ).should( 'contain', 'Apply' ).click(); cy.log( 'Permanently delete 2 forms using the Delete Permanetly option' ); @@ -52,21 +63,27 @@ describe( 'Deleting forms', () => { cy.get( '#bulk-action-selector-top' ).should( 'contain', 'Bulk Actions' ).select( 'bulk_delete' ); cy.get( '#doaction' ).should( 'contain', 'Apply' ).click(); cy.get( '.frm-confirm-msg' ).should( 'contain', 'ALL selected forms and their entries will be permanently deleted. Want to proceed?' ); - cy.contains( 'a.button-secondary', 'Cancel' ) - .click( { force: true } ); + // cy.contains() (not cy.get()) is required here - two confirm banners with their own + // `a.button-secondary` Cancel link both exist in the DOM at once, so a plain cy.get() + // matches 2 elements and cy.click() rejects a multi-element subject. + cy.contains( 'a.button-secondary', 'Cancel' ).should( 'be.visible' ).click(); cy.get( '#doaction' ).should( 'contain', 'Apply' ).click(); cy.get( '#frm-confirmed-click' ).should( 'contain', 'Confirm' ).click(); cy.get( '.trash > a' ).should( 'contain.text', 'Trash' ) .find( '.count' ).should( 'contain.text', '(3)' ); cy.get( '.published > a' ).should( 'contain.text', 'My Forms' ) - .find( '.count' ).should( 'contain.text', '(0)' ); + .find( '.count' ).then( $count => { + expect( $count.text() ).to.eq( baselinePublishedCount ); + } ); cy.log( 'Empty trash' ); cy.get( '#delete_all' ).should( 'contain', 'Empty Trash' ).click(); cy.get( '.trash > a' ).should( 'contain.text', 'Trash' ) .find( '.count' ).should( 'contain.text', '(0)' ); cy.get( '.published > a' ).should( 'contain.text', 'My Forms' ) - .find( '.count' ).should( 'contain.text', '(0)' ); + .find( '.count' ).then( $count => { + expect( $count.text() ).to.eq( baselinePublishedCount ); + } ); cy.get( '.colspanchange > p' ).should( 'contain', 'No forms found in the trash.' ); } ); } ); diff --git a/tests/cypress/e2e/Forms/duplicateForm.cy.js b/tests/cypress/e2e/Forms/duplicateForm.cy.js index a94aedec4a..f8d3afd2c7 100644 --- a/tests/cypress/e2e/Forms/duplicateForm.cy.js +++ b/tests/cypress/e2e/Forms/duplicateForm.cy.js @@ -14,27 +14,45 @@ describe( 'Duplicating a form from the form list page', () => { cy.log( 'Find the visible element with class duplicate within the hovered row and click it' ); cy.wrap( $row ).within( () => { - cy.get( '.row-actions .duplicate .frm-trash-link' ).should( 'be.visible' ).click( { force: true } ); + // WP core only reveals row-actions on a real CSS `:hover` (`.row-actions` is + // `position: relative; left: -9999em` until `tr:hover`) - make it actionable the + // way the real hover would, then click normally. Chained in one continuous command + // so there's no window between the reset and the click for a re-render to undo it. + cy.get( '.row-actions' ) + .invoke( 'css', 'position', 'static' ) + .find( '.duplicate .frm-trash-link' ) + .should( 'be.visible' ) + .click(); } ); cy.get( "a[aria-label='Close']", { timeout: 5000 } ).click(); cy.log( 'Locate rows containing the text - Test Form and count them' ); cy.get( '#the-list tr:contains("Test Form")' ).then( $rows => { - expect( $rows.length ).to.equal( 2 ); + expect( $rows ).to.have.lengthOf( 2 ); } ); } ); + } ); - cy.log( 'Teardown - Delete Test Form and its duplicate' ); - cy.deleteForm(); + // A separate afterEach (rather than teardown steps at the end of the `it` block) so cleanup + // still runs even if the assertion above throws - otherwise a failed run leaves both the + // original and the duplicate "Test Form" behind for every later spec that assumes a clean + // list (see the cross-spec "Test Form" leakage this caused). + afterEach( () => { + cy.log( 'Teardown - Move every Test Form row to the trash, how many are left doesn\'t matter' ); + cy.visit( '/wp-admin/admin.php?page=formidable' ); + cy.get( 'body' ).then( $body => { + const testFormRows = $body.find( '#the-list tr' ).filter( ( _, element ) => Cypress.$( element ).text().includes( 'Test Form' ) ); - cy.log( 'Delete duplicated form' ); - cy.contains( '#the-list tr', 'Test Form' ).trigger( 'mouseover' ).then( $row => { - cy.wrap( $row ).within( () => { - cy.get( '.row-actions .trash .frm-trash-link' ).should( 'be.visible' ).click( { force: true } ); + if ( testFormRows.length === 0 ) { + return; + } + + cy.wrap( testFormRows ).each( $row => { + cy.wrap( $row ).find( '.check-column input[type="checkbox"]' ).check(); } ); - cy.get( "div[role='dialog']" ).should( 'contain', 'Do you want to move this form to the trash?' ); - cy.xpath( "//a[@id='frm-confirmed-click']" ).should( 'contain', 'Confirm' ).click( { force: true } ); + cy.get( '#bulk-action-selector-top' ).select( 'Move to Trash' ); + cy.get( '#doaction' ).should( 'contain', 'Apply' ).click(); } ); } ); } ); diff --git a/tests/cypress/e2e/Forms/fieldsInFormBuilder.cy.js b/tests/cypress/e2e/Forms/fieldsInFormBuilder.cy.js index 5bfb99f95b..619475ab07 100644 --- a/tests/cypress/e2e/Forms/fieldsInFormBuilder.cy.js +++ b/tests/cypress/e2e/Forms/fieldsInFormBuilder.cy.js @@ -6,12 +6,32 @@ describe( 'Fields in the form builder', () => { cy.viewport( 1280, 720 ); } ); + // Shared by the tests below - the "Add Fields" sidebar icon is a plain, always-visible link + // with no hover gating, so a simple visibility wait is enough (no force needed). + const createField = ( fieldId, fieldType ) => { + cy.log( `Create a ${ fieldType } field` ); + cy.get( `li[id="${ fieldId }"] a[title="${ fieldType }"]` ).should( 'be.visible' ).click(); + }; + it( 'should create, duplicate a field from each type and delete them', () => { const createAndDuplicateField = ( fieldId, fieldType ) => { cy.log( `Create a ${ fieldType } field and duplicate it` ); cy.get( `li[id="${ fieldId }"] a[title="${ fieldType }"]` ).click(); - cy.get( `li[data-ftype="${ fieldId }"] [id^="field_"][id$="_inner_container"] > .frm-field-action-icons > .dropdown > .frm_bstooltip > .frmsvg > use`, { timeout: 10000 } ).click( { force: true } ); - cy.get( `li[data-ftype="${ fieldId }"] .frm_clone_field > span` ).should( 'contain', 'Duplicate' ).click( { force: true } ); + // .frm-field-action-icons is also .frm-show-hover (opacity: 0 by default; see + // resources/scss/admin/components/sorting/_sorting-display.scss), only revealed on a + // real CSS :hover of the field row or when the field is .selected - it's still genuinely + // clickable underneath, so reveal it the same way the row-actions helpers in commands.js + // do, instead of forcing through the opacity check. + cy.get( `li[data-ftype="${ fieldId }"] [id^="field_"][id$="_inner_container"] > .frm-field-action-icons`, { timeout: 10000 } ) + .invoke( 'css', 'opacity', 1 ) + .find( '.dropdown > .frm_bstooltip > .frmsvg > use' ) + .first() + .should( 'be.visible' ) + .click(); + // The dropdown menu opens via a Bootstrap JS toggle (a real click, not hover-gated), so + // once it's open the item is genuinely clickable - wait for it to be visible instead of + // forcing through the open transition. + cy.get( `li[data-ftype="${ fieldId }"] .frm_clone_field > span` ).should( 'be.visible' ).and( 'contain', 'Duplicate' ).click(); cy.get( `li[data-type="${ fieldId }"]` ).should( 'have.length', 2 ); const originalField = cy.get( `li[data-type="${ fieldId }"]:first` ); @@ -21,21 +41,30 @@ describe( 'Fields in the form builder', () => { const removeField = field => { field.within( () => { - cy.get( '.frm-field-action-icons .dropdown' ) - .trigger( 'mouseover' ); - - cy.get( '.frm-field-action-icons .dropdown .frm-hover-icon .frmsvg' ) - .click( { force: true } ); - + // Same .frm-show-hover opacity gate as the toggle above - reveal it first. + cy.get( '.frm-field-action-icons' ) + .invoke( 'css', 'opacity', 1 ) + .find( '.dropdown .frm-hover-icon .frmsvg' ) + .first() + .should( 'be.visible' ) + .click(); + + // The menu is open via the click above (not hover-gated), so wait for the item to + // be visible instead of forcing through the open transition. cy.get( '.frm-dropdown-menu .frm_delete_field' ) - .should( 'contain', 'Delete' ) - .click( { force: true } ); + .should( 'be.visible' ) + .and( 'contain', 'Delete' ) + .click(); } ); - cy.get( '.postbox a[id="frm-confirmed-click"]' ) - .contains( 'Confirm' ) + // Plain cy.get() by id (an id is unique) rather than cy.get().contains() - the latter + // can resolve to a narrower descendant node than the clickable link itself, which is + // what forced force here. Plain cy.get() on this id works unforced elsewhere in the + // suite. + cy.get( '#frm-confirmed-click' ) .should( 'be.visible' ) - .click( { force: true } ); + .and( 'contain', 'Confirm' ) + .click(); cy.get( `li[data-type="${ field }"]` ).should( 'not.exist' ); }; @@ -43,7 +72,9 @@ describe( 'Fields in the form builder', () => { cy.contains( '#the-list tr', 'Test Form' ).trigger( 'mouseover' ).then( $row => { cy.wrap( $row ).within( () => { cy.get( '.column-name .row-title' ).should( 'exist' ).and( 'be.visible' ).then( $elem => { - cy.wrap( $elem ).click( { force: true } ); + // Plain click - verified via document.elementFromPoint() that the link is the + // topmost element at its own coordinates, not covered by anything. No force needed. + cy.wrap( $elem ).click(); } ); } ); } ); @@ -80,16 +111,20 @@ describe( 'Fields in the form builder', () => { } ); it( 'should rename a field from each type', () => { - const createField = ( fieldId, fieldType ) => { - cy.log( `Create a ${ fieldType } field` ); - cy.get( `li[id="${ fieldId }"] a[title="${ fieldType }"]` ).click( { force: true } ); - }; - const renameField = ( fieldId, fieldType, fieldValue ) => { cy.log( `Rename a ${ fieldType } field` ); - cy.get( `li[data-ftype="${ fieldId }"] [id^="field_"][id$="_inner_container"] > .frm-field-action-icons > .dropdown > .frm_bstooltip > .frmsvg > use`, { timeout: 10000 } ).click( { force: true } ); - cy.get( `li[data-ftype="${ fieldId }"] .frm_select_field > span` ).should( 'contain', 'Field Settings' ).click( { force: true } ); - cy.get( `div[id^="frm-single-settings-"] input[value="${ fieldValue }"]`, { timeout: 10000 } ).should( 'be.visible' ).clear( { force: true } ).type( `${ fieldType } Updated`, { force: true } ); + // See the .frm-show-hover opacity note on the field-row "more options" toggle above. + cy.get( `li[data-ftype="${ fieldId }"] [id^="field_"][id$="_inner_container"] > .frm-field-action-icons`, { timeout: 10000 } ) + .invoke( 'css', 'opacity', 1 ) + .find( '.dropdown > .frm_bstooltip > .frmsvg > use' ) + .first() + .should( 'be.visible' ) + .click(); + cy.get( `li[data-ftype="${ fieldId }"] .frm_select_field > span` ).should( 'be.visible' ).and( 'contain', 'Field Settings' ).click(); + // The settings panel opens via a bounded jQuery slideDown() (admin.js) - wait for the + // input to be visible (Cypress retries until it has settled into the flow), then interact + // normally. + cy.get( `div[id^="frm-single-settings-"] input[value="${ fieldValue }"]`, { timeout: 10000 } ).should( 'be.visible' ).clear().type( `${ fieldType } Updated` ); }; cy.openForm(); @@ -121,16 +156,18 @@ describe( 'Fields in the form builder', () => { it( 'should set fields as required and validate them in frontend', () => { const fieldTypes = [ 'Text', 'Paragraph', 'Checkboxes', 'Radio Buttons', 'Dropdown', 'Email', 'Website/URL', 'Number', 'Name', 'Phone' ]; - const createField = ( fieldId, fieldType ) => { - cy.log( `Create a ${ fieldType } field` ); - cy.get( `li[id="${ fieldId }"] a[title="${ fieldType }"]` ).click( { force: true } ); - }; - const requiredField = ( fieldId, fieldType ) => { cy.log( `Set ${ fieldType } field as require` ); - cy.get( `li[data-ftype="${ fieldId }"] [id^="field_"][id$="_inner_container"] > .frm-field-action-icons > .dropdown > .frm_bstooltip > .frmsvg > use`, { timeout: 10000 } ).click( { force: true } ); - cy.get( `li[data-ftype="${ fieldId }"] .frm_select_field > span` ).should( 'contain', 'Field Settings' ).click( { force: true } ); - cy.get( 'input.frm_req_field[type="checkbox"]' ).check( { force: true } ); + // See the .frm-show-hover opacity note on the field-row "more options" toggle above. + cy.get( `li[data-ftype="${ fieldId }"] [id^="field_"][id$="_inner_container"] > .frm-field-action-icons`, { timeout: 10000 } ) + .invoke( 'css', 'opacity', 1 ) + .find( '.dropdown > .frm_bstooltip > .frmsvg > use' ) + .first() + .should( 'be.visible' ) + .click(); + cy.get( `li[data-ftype="${ fieldId }"] .frm_select_field > span` ).should( 'be.visible' ).and( 'contain', 'Field Settings' ).click(); + // Same slideDown()-driven settings panel as elsewhere in this file. + cy.get( 'input.frm_req_field[type="checkbox"]' ).should( 'be.visible' ).check(); }; cy.openForm(); @@ -154,7 +191,9 @@ describe( 'Fields in the form builder', () => { } ); cy.log( 'Update form' ); - cy.get( '#frm_submit_side_top' ).should( 'contain', 'Update' ).click( { force: true } ); + // Plain #frm_submit_side_top "Update" click - no force needed here (see the identical + // unforced click used for this same button elsewhere in the suite, e.g. formsSettings.cy.js). + cy.get( '#frm_submit_side_top' ).should( 'contain', 'Update' ).click(); cy.log( 'Click on Preview - Blank Page' ); cy.get( '#frm-previewDrop', { timeout: 5000 } ).should( 'contain', 'Preview' ).click(); @@ -190,23 +229,36 @@ describe( 'Fields in the form builder', () => { it( 'should validate forms with javascript setting', () => { cy.openForm(); cy.log( `Create a text field and set it as required` ); - cy.get( `li[id="text"] a[title="Text"]` ).click( { force: true } ); - cy.get( `li[data-ftype="text"] [id^="field_"][id$="_inner_container"] > .frm-field-action-icons > .dropdown > .frm_bstooltip > .frmsvg > use`, { timeout: 10000 } ).click( { force: true } ); - cy.get( `li[data-ftype="text"] .frm_select_field > span` ).should( 'contain', 'Field Settings' ).click( { force: true } ); - cy.get( '.frm_field_list div[id^="frm-single-settings-"] .frm_grid_container .frm-hide-empty input[type="checkbox"]', { timeout: 10000 } ).check( { force: true } ); + // Plain, always-visible sidebar link - see the identical unforced click elsewhere in this file. + cy.get( `li[id="text"] a[title="Text"]` ).should( 'be.visible' ).click(); + // See the .frm-show-hover opacity note on the field-row "more options" toggle elsewhere in + // this file. + cy.get( `li[data-ftype="text"] [id^="field_"][id$="_inner_container"] > .frm-field-action-icons`, { timeout: 10000 } ) + .invoke( 'css', 'opacity', 1 ) + .find( '.dropdown > .frm_bstooltip > .frmsvg > use' ) + .first() + .should( 'be.visible' ) + .click(); + cy.get( `li[data-ftype="text"] .frm_select_field > span` ).should( 'be.visible' ).and( 'contain', 'Field Settings' ).click(); + // Same slideDown()-driven settings panel as elsewhere in this file. + cy.get( '.frm_field_list div[id^="frm-single-settings-"] .frm_grid_container .frm-hide-empty input[type="checkbox"]', { timeout: 10000 } ).should( 'be.visible' ).check(); cy.log( 'Create a phone and email field' ); - cy.get( `li[id="email"] a[title="Email"]` ).click( { force: true } ); - cy.get( `li[id="phone"] a[title="Phone"]` ).click( { force: true } ); + cy.get( `li[id="email"] a[title="Email"]` ).should( 'be.visible' ).click(); + cy.get( `li[id="phone"] a[title="Phone"]` ).should( 'be.visible' ).click(); cy.log( 'Update form' ); - cy.get( '#frm_submit_side_top' ).should( 'contain', 'Update' ).click( { force: true } ); + // Plain #frm_submit_side_top "Update" click - no force needed, see the note above. + cy.get( '#frm_submit_side_top' ).should( 'contain', 'Update' ).click(); cy.log( "Enabling the 'Validate this form with javascript' setting" ); cy.xpath( "//ul[@class='frm_form_nav']//a[contains(text(),'Settings')]" ).should( 'contain', 'Settings' ).click(); cy.get( ':nth-child(3) > td > .frm_inline_block', { timeout: 5000 } ).should( 'contain', 'Validate this form with javascript' ); - cy.get( '#js_validate' ).click( { force: true } ); - cy.get( '#frm_submit_side_top' ).should( 'contain', 'Update' ).click( { force: true } ); + // This checkbox's own