@@ -36,8 +36,7 @@ export function addFormChild( name ) {
3636export function loginToSite ( ) {
3737 return goTo ( '/wp-login.php' , true )
3838 . then ( ( ) => {
39- cy . wait ( 250 ) ;
40-
39+ // Wait for login form to be ready
4140 cy . get ( '#user_login' ) . type ( Cypress . env ( 'wpUsername' ) ) ;
4241 cy . get ( '#user_pass' ) . type ( Cypress . env ( 'wpPassword' ) ) ;
4342 cy . get ( '#wp-submit' ) . click ( ) ;
@@ -137,8 +136,31 @@ export function addBlockToPost( blockName, clearEditor = false ) {
137136 // Make sure the block was added to our page
138137 cy . get ( `[class*="-visual-editor"] [data-type="${ blockName } "]` ) . should ( 'exist' ) ;
139138
140- // Give a short delay for blocks to render.
141- cy . wait ( 250 ) ;
139+ // Instead of arbitrary wait, wait for the block to be fully rendered
140+ // This means waiting for the block to not have loading states
141+ cy . get ( `[data-type="${ blockName } "]` ) . should ( ( $block ) => {
142+ // The block should be stable (not detaching/reattaching)
143+ expect ( $block ) . to . have . length . at . least ( 1 ) ;
144+ // The block should have its content loaded (not just a placeholder)
145+ expect ( $block . html ( ) ) . to . have . length . greaterThan ( 50 ) ;
146+ } ) ;
147+
148+ // Also ensure the block has finished its initial render cycle
149+ cy . get ( `[data-type="${ blockName } "]` ) . should ( 'not.have.class' , 'is-loading' ) ;
150+
151+ // For gallery blocks specifically, wait for upload UI to be ready and stable
152+ if ( blockName . includes ( 'gallery' ) ) {
153+ // Wait for the upload interface to be available
154+ cy . get ( `[data-type="${ blockName } "]` ) . should ( ( $block ) => {
155+ const text = $block . text ( ) ;
156+ expect ( text ) . to . satisfy ( ( content ) =>
157+ content . includes ( 'Upload' ) || content . includes ( 'Select' ) || content . includes ( 'Media Library' )
158+ ) ;
159+ } ) ;
160+
161+ // Gallery blocks need a small stabilization period to prevent DOM detachment.
162+ cy . wait ( 150 ) ;
163+ }
142164}
143165
144166export function addNewGroupToPost ( ) {
@@ -154,8 +176,7 @@ export function addNewGroupToPost() {
154176 cy . get ( '.block-editor-inserter__search-input,input.block-editor-inserter__search, .components-search-control__input' ) . click ( ) . type ( 'group' ) ;
155177 }
156178
157- cy . wait ( 1000 ) ;
158-
179+ // Wait for search results to appear
159180 cy . get ( '.block-editor-block-types-list__list-item' ) . contains ( 'Group' ) . click ( ) ;
160181
161182 // Make sure the block was added to our page
@@ -172,11 +193,22 @@ export function addNewGroupToPost() {
172193 * From inside the WordPress editor open the CoBlocks Gutenberg editor panel
173194 */
174195export function savePage ( ) {
175- if ( isWP66AtLeast ( ) ) {
176- cy . get ( '.editor-header__settings button.is-primary' ) . click ( ) ;
177- } else {
178- cy . get ( '.edit-post-header__settings button.is-primary' ) . click ( ) ;
179- }
196+ // Try multiple selectors for cross-version compatibility (WP 6.5-6.8.2)
197+ cy . get ( 'body' ) . then ( ( $body ) => {
198+ if ( $body . find ( '.editor-header__settings button.is-primary' ) . length > 0 ) {
199+ // WP 6.6+ selector
200+ cy . get ( '.editor-header__settings button.is-primary' ) . click ( ) ;
201+ } else if ( $body . find ( '.edit-post-header__settings button.is-primary' ) . length > 0 ) {
202+ // WP 6.5 and earlier selector
203+ cy . get ( '.edit-post-header__settings button.is-primary' ) . click ( ) ;
204+ } else if ( $body . find ( '.edit-post-header-toolbar__settings button.is-primary' ) . length > 0 ) {
205+ // Alternative pattern
206+ cy . get ( '.edit-post-header-toolbar__settings button.is-primary' ) . click ( ) ;
207+ } else {
208+ // Generic fallback
209+ cy . get ( 'button.is-primary' ) . contains ( / s a v e | p u b l i s h | u p d a t e / i ) . click ( ) ;
210+ }
211+ } ) ;
180212
181213 cy . get ( '.components-editor-notices__snackbar' , { timeout : 120000 } ) . should ( 'not.be.empty' ) ;
182214
@@ -208,23 +240,42 @@ export function checkForBlockErrors( blockName ) {
208240 * View the currently edited page on the front of site
209241 */
210242export function viewPage ( ) {
211- cy . get ( 'button[aria-label="Settings"]' ) . then ( ( settingsButton ) => {
212- if ( ! Cypress . $ ( settingsButton ) . hasClass ( 'is-pressed' ) && ! Cypress . $ ( settingsButton ) . hasClass ( 'is-toggled' ) ) {
213- cy . get ( settingsButton ) . click ( ) ;
243+ // Open settings panel if not already open
244+ cy . get ( 'body' ) . then ( ( $body ) => {
245+ const settingsButton = $body . find ( 'button[aria-label="Settings"]' ) ;
246+ if ( settingsButton . length > 0 && ! settingsButton . hasClass ( 'is-pressed' ) && ! settingsButton . hasClass ( 'is-toggled' ) ) {
247+ cy . get ( 'button[aria-label="Settings"]' ) . click ( ) ;
214248 }
215249 } ) ;
216250
217- if ( isWP65AtLeast ( ) ) {
218- cy . get ( '[data-tab-id="edit-post/document"] ' ) ;
251+ // Wait for the settings panel to be visible
252+ cy . get ( '.interface-interface-skeleton__sidebar' ) . should ( 'be.visible ' ) ;
219253
220- cy . get ( '.editor-post-url__panel-dropdown button' ) . click ( ) ;
221- } else {
222- cy . get ( 'button[data-label="Post"]' ) ;
254+ // Try multiple approaches to find the post URL
255+ cy . get ( 'body' ) . then ( ( $body ) => {
256+ if ( $body . find ( '[data-tab-id="edit-post/document"]' ) . length > 0 ) {
257+ // WP 6.5+ approach
258+ cy . get ( '[data-tab-id="edit-post/document"]' ) . click ( ) ;
259+ // Wait for document tab to become active
260+ cy . get ( '[data-tab-id="edit-post/document"]' ) . should ( 'have.attr' , 'aria-selected' , 'true' ) ;
261+ }
223262
224- cy . get ( '.edit-post-post-url__dropdown button' ) . click ( ) ;
225- }
263+ // Try different selectors for the URL dropdown
264+ if ( $body . find ( '.editor-post-url__panel-dropdown button' ) . length > 0 ) {
265+ cy . get ( '.editor-post-url__panel-dropdown button' ) . click ( ) ;
266+ } else if ( $body . find ( '.edit-post-post-url__dropdown button' ) . length > 0 ) {
267+ cy . get ( '.edit-post-post-url__dropdown button' ) . click ( ) ;
268+ } else if ( $body . find ( 'button[data-label="Post"]' ) . length > 0 ) {
269+ cy . get ( 'button[data-label="Post"]' ) . click ( ) ;
270+ cy . get ( '.edit-post-post-url__dropdown button' ) . click ( ) ;
271+ } else {
272+ // Fallback: look for any URL-related dropdown
273+ cy . get ( '[class*="url"], [class*="permalink"]' ) . find ( 'button' ) . first ( ) . click ( ) ;
274+ }
275+ } ) ;
226276
227- cy . get ( '.editor-post-url__link' ) . then ( ( pageLink ) => {
277+ // Get the link and visit it
278+ cy . get ( '.editor-post-url__link, .edit-post-post-url__link' ) . then ( ( pageLink ) => {
228279 const linkAddress = Cypress . $ ( pageLink ) . attr ( 'href' ) ;
229280 cy . visit ( linkAddress ) ;
230281 } ) ;
@@ -239,14 +290,17 @@ export function editPage() {
239290}
240291
241292/**
242- * Clear all blocks from the editor
293+ * Clear all blocks from the editor and wait for them to be fully removed
243294 */
244295export function clearBlocks ( ) {
245296 getWPDataObject ( ) . then ( ( data ) => {
246297 data . dispatch ( 'core/block-editor' ) . removeBlocks (
247298 data . select ( 'core/block-editor' ) . getBlocks ( ) . map ( ( block ) => block . clientId )
248299 ) ;
249300 } ) ;
301+
302+ // Simple wait and verify no blocks remain
303+ cy . get ( '.block-editor-block-list__layout' ) . should ( 'not.contain' , '[data-type]' ) ;
250304}
251305
252306/**
@@ -294,12 +348,10 @@ export function setNewBlockStyle( style ) {
294348 */
295349export function selectBlock ( name ) {
296350 /**
297- * There are network requests taking place to the REST API to get the blocks and block patterns.
298- * Sometimes these requests occur and other times they are cached and are not called.
299- * For that reason is difficult to assert against those requests from core code.
300- * We introduce an arbitrary wait to avoid a race condition by interacting too quickly.
351+ * Wait for the block to be available in the editor before attempting to select it.
352+ * This replaces the arbitrary wait with a proper assertion.
301353 */
302- cy . wait ( 600 ) ;
354+ cy . get ( `[data-type*=" ${ name } "], [data-title*=" ${ name } "]` ) . should ( 'exist' ) ;
303355
304356 let id = '' ; // The block client ID.
305357 cy . window ( ) . then ( ( win ) => {
@@ -321,7 +373,8 @@ export function selectBlock( name ) {
321373 cy . window ( ) . then ( ( win ) => {
322374 win . wp . data . dispatch ( 'core/edit-post' ) . openGeneralSidebar ( 'edit-post/block' ) ;
323375 } ) ;
324- cy . wait ( 600 ) ;
376+ // Wait for sidebar to be visible
377+ cy . get ( '.interface-interface-skeleton__sidebar' ) . should ( 'be.visible' ) ;
325378}
326379
327380/**
@@ -533,11 +586,28 @@ export function openHeadingToolbarAndSelect( headingLevel ) {
533586 * @param {string } checkboxLabelText The checkbox label text. eg: Drop Cap
534587 */
535588export function toggleSettingCheckbox ( checkboxLabelText ) {
536- cy . get ( '.components-toggle-control__label' )
589+ // Atomic approach using should() to avoid DOM detachment
590+ cy . get ( 'label' )
537591 . contains ( checkboxLabelText )
538- . closest ( '.components-base-control__field' )
539- . find ( '.components-form-toggle__input' )
540- . click ( ) ;
592+ . should ( ( $label ) => {
593+ const forAttr = $label . attr ( 'for' ) ;
594+ if ( forAttr ) {
595+ // Find and click the associated input directly in the callback
596+ const input = Cypress . $ ( `#${ forAttr } ` ) [ 0 ] ;
597+ if ( input ) {
598+ input . click ( ) ;
599+ }
600+ } else {
601+ // Fallback: find input within the same control
602+ const control = $label . closest ( '.components-base-control, .components-toggle-control' ) [ 0 ] ;
603+ if ( control ) {
604+ const input = Cypress . $ ( control ) . find ( '.components-form-toggle__input, input[type="checkbox"]' ) [ 0 ] ;
605+ if ( input ) {
606+ input . click ( ) ;
607+ }
608+ }
609+ }
610+ } ) ;
541611}
542612
543613/**
@@ -638,3 +708,155 @@ export function getIframeBody( containerClass ) {
638708export const sidebarClass = ( ) => {
639709 return isWP66AtLeast ( ) ? '.editor-sidebar__panel' : '.edit-post-sidebar' ;
640710} ;
711+
712+ /**
713+ * Click the settings/inspector button with cross-version compatibility
714+ */
715+ export function openInspectorPanel ( ) {
716+ // Try multiple selectors for cross-version compatibility (WP 6.5-6.8.2)
717+ cy . get ( 'body' ) . then ( ( $body ) => {
718+ if ( $body . find ( '.editor-header__settings' ) . length > 0 ) {
719+ // WP 6.6+ selector
720+ cy . get ( '.editor-header__settings' ) . click ( ) ;
721+ } else if ( $body . find ( '.edit-post-header__settings' ) . length > 0 ) {
722+ // WP 6.5 and earlier selector
723+ cy . get ( '.edit-post-header__settings' ) . click ( ) ;
724+ } else if ( $body . find ( '.edit-post-header-toolbar__settings' ) . length > 0 ) {
725+ // Alternative pattern
726+ cy . get ( '.edit-post-header-toolbar__settings' ) . click ( ) ;
727+ } else {
728+ // Generic fallback using aria-label
729+ cy . get ( '[aria-label*="Settings"], [aria-label*="Inspector"]' ) . click ( ) ;
730+ }
731+ } ) ;
732+
733+ // Wait for sidebar to be visible and panels to load
734+ cy . get ( '.interface-interface-skeleton__sidebar' ) . should ( 'be.visible' ) ;
735+
736+ // In newer WP versions, we need to click the "Block" tab to see block settings
737+ cy . get ( 'body' ) . then ( ( $body ) => {
738+ const blockTab = $body . find ( '[data-tab-id="edit-post/block"]' ) ;
739+ if ( blockTab . length > 0 ) {
740+ // Click Block tab if it exists and isn't already selected
741+ if ( ! blockTab . attr ( 'aria-selected' ) || blockTab . attr ( 'aria-selected' ) === 'false' ) {
742+ cy . get ( '[data-tab-id="edit-post/block"]' ) . click ( ) ;
743+ // Wait for panels to appear after clicking Block tab
744+ cy . get ( '.components-panel__body-title' ) . should ( 'have.length.at.least' , 1 ) ;
745+ }
746+ } else {
747+ // For older versions without tabs, wait for panels to be visible
748+ cy . get ( '.components-panel__body-title' ) . should ( 'have.length.at.least' , 1 ) ;
749+ }
750+ } ) ;
751+ }
752+
753+ /**
754+ * Open a specific panel in the inspector with cross-version compatibility
755+ *
756+ * @param {string } panelName - The name of the panel to open (case-insensitive)
757+ */
758+ export function openInspectorPanelSection ( panelName ) {
759+ // First ensure panels are loaded
760+ cy . get ( '.components-panel__body-title' ) . should ( 'have.length.at.least' , 1 ) ;
761+
762+ // Find the panel button and expand if needed
763+ cy . get ( '.components-panel__body-title button' )
764+ . contains ( new RegExp ( panelName , 'i' ) )
765+ . then ( ( $button ) => {
766+ const isExpanded = $button . attr ( 'aria-expanded' ) === 'true' ;
767+
768+ if ( ! isExpanded ) {
769+ cy . wrap ( $button ) . click ( ) ;
770+ // Wait for panel to expand by checking aria-expanded again
771+ cy . get ( '.components-panel__body-title button' )
772+ . contains ( new RegExp ( panelName , 'i' ) )
773+ . should ( 'have.attr' , 'aria-expanded' , 'true' ) ;
774+ }
775+ } ) ;
776+
777+ // For Link Settings specifically, wait for the SelectControl to render
778+ if ( panelName . toLowerCase ( ) . includes ( 'link' ) ) {
779+ // Simply check that a select element exists somewhere in the expanded panel area
780+ cy . get ( '.components-panel__body select' ) . should ( 'exist' ) ;
781+ }
782+ }
783+
784+ /**
785+ * Wait for and interact with the Link Settings dropdown
786+ * This is purpose-built for the gallery link functionality
787+ *
788+ * @param {string } linkType The type of link to select (e.g., 'custom', 'media', 'attachment', 'none')
789+ */
790+ export function selectLinkOption ( linkType ) {
791+ // Simple approach: find select element that should exist after panel expansion
792+ cy . get ( '.components-panel__body select' )
793+ . should ( 'be.visible' )
794+ . select ( linkType ) ;
795+ }
796+
797+ /**
798+ * Complete helper function for testing custom link functionality in gallery blocks
799+ * Handles the full flow from block selection to URL input and verification
800+ *
801+ * @param {string } blockName - The block name (e.g., 'coblocks/gallery-offset', 'coblocks/gallery-collage')
802+ * @param {string } customUrl - The URL to set for the custom link
803+ * @param {string } imageSelector - CSS selector for the image element to verify link on
804+ */
805+ export function setGalleryCustomLink ( blockName , customUrl , imageSelector = 'img' ) {
806+ // Use the exact working workflow from debug test
807+
808+ // Step 1: Block selection
809+ cy . get ( `[data-type="${ blockName } "]` ) . click ( ) ;
810+
811+ // Step 2: Open inspector and ensure it stays open
812+ cy . get ( 'body' ) . then ( ( $body ) => {
813+ const sidebar = $body . find ( '.interface-interface-skeleton__sidebar' ) ;
814+ if ( ! sidebar . is ( ':visible' ) ) {
815+ cy . get ( '.editor-header__settings, .edit-post-header__settings' ) . click ( ) ;
816+ }
817+ } ) ;
818+ cy . get ( '.interface-interface-skeleton__sidebar' ) . should ( 'be.visible' ) ;
819+
820+ // Step 3: Ensure Block tab is active
821+ cy . get ( '[data-tab-id="edit-post/block"]' ) . then ( ( $tab ) => {
822+ if ( $tab . attr ( 'aria-selected' ) !== 'true' ) {
823+ cy . get ( '[data-tab-id="edit-post/block"]' ) . click ( ) ;
824+ }
825+ } ) ;
826+ cy . get ( '[data-tab-id="edit-post/block"]' ) . should ( 'have.attr' , 'aria-selected' , 'true' ) ;
827+
828+ // Step 4: Expand Link Settings panel
829+ cy . get ( '.components-panel__body-title button' ) . contains ( / l i n k / i ) . then ( ( $button ) => {
830+ const isExpanded = $button . attr ( 'aria-expanded' ) === 'true' ;
831+ if ( ! isExpanded ) {
832+ cy . wrap ( $button ) . click ( ) ;
833+ }
834+ } ) ;
835+ cy . get ( '.components-panel__body-title button' ) . contains ( / l i n k / i ) . should ( 'have.attr' , 'aria-expanded' , 'true' ) ;
836+
837+ // Step 5: Select custom link type
838+ cy . get ( '.components-panel__body' ) . contains ( / l i n k / i ) . closest ( '.components-panel__body' ) . within ( ( ) => {
839+ cy . get ( 'select' ) . select ( 'custom' ) ;
840+ } ) ;
841+
842+ // Step 6: Click the image to show URL input
843+ if ( blockName . includes ( 'collage' ) ) {
844+ cy . get ( `[data-type="${ blockName } "] .wp-block-coblocks-gallery-collage__item` ) . first ( ) . click ( ) ;
845+ cy . get ( `[data-type="${ blockName } "] img` ) . first ( ) . click ( { force : true } ) ;
846+ } else {
847+ cy . get ( `[data-type="${ blockName } "]` ) . within ( ( ) => {
848+ cy . get ( imageSelector ) . first ( ) . click ( { force : true } ) ;
849+ } ) ;
850+ }
851+
852+ // Step 7: Find and use the URL input
853+ cy . get ( '.block-editor-url-input input:visible' ) . should ( 'exist' ) . clear ( ) . type ( customUrl ) ;
854+ cy . get ( 'button[type="submit"]:visible' ) . click ( ) ;
855+
856+ // Step 8: Verify
857+ cy . get ( `[data-type="${ blockName } "]` ) . within ( ( ) => {
858+ cy . get ( imageSelector ) . first ( )
859+ . should ( 'have.attr' , 'data-imglink' )
860+ . and ( 'include' , customUrl ) ;
861+ } ) ;
862+ }
0 commit comments