@@ -798,5 +798,253 @@ BrowserHelper.setMethod(async function writeCoverageFiles(output_dir, prefix) {
798798 return coverages . length ;
799799} ) ;
800800
801+ /**
802+ * Wait for the Hawkejs scene to be ready.
803+ * This is important when testing pages with custom elements or client-side rendering.
804+ *
805+ * The scene is ready when:
806+ * - The hawkejs object exists
807+ * - The scene has been initialized
808+ * - The general_renderer is available
809+ *
810+ * @author Jelle De Loecker <jelle@elevenways.be>
811+ * @since 1.4.1
812+ * @version 1.4.1
813+ *
814+ * @param {number } timeout Maximum time to wait in ms (default: 5000)
815+ *
816+ * @return {Promise<boolean> }
817+ */
818+ BrowserHelper . setMethod ( async function waitForSceneReady ( timeout = 5000 ) {
819+
820+ if ( ! this . page ) {
821+ throw new Error ( 'Browser not loaded. Call load() or goto() first.' ) ;
822+ }
823+
824+ return this . evaluate ( ( timeout ) => {
825+ return new Promise ( ( resolve ) => {
826+
827+ const isReady = ( ) => {
828+ return typeof hawkejs !== 'undefined' &&
829+ hawkejs . scene &&
830+ hawkejs . scene . general_renderer ;
831+ } ;
832+
833+ if ( isReady ( ) ) {
834+ return resolve ( true ) ;
835+ }
836+
837+ const start = Date . now ( ) ;
838+
839+ const check = ( ) => {
840+ if ( isReady ( ) ) {
841+ return resolve ( true ) ;
842+ }
843+
844+ if ( Date . now ( ) - start > timeout ) {
845+ return resolve ( false ) ;
846+ }
847+
848+ setTimeout ( check , 50 ) ;
849+ } ;
850+
851+ check ( ) ;
852+ } ) ;
853+ } , timeout ) ;
854+ } ) ;
855+
856+ /**
857+ * Wait for an element to appear in the DOM using MutationObserver.
858+ * This is the proper way to wait for Alchemy custom elements that render asynchronously.
859+ *
860+ * Unlike Puppeteer's waitForSelector, this:
861+ * - Uses MutationObserver (efficient, no polling)
862+ * - Returns element info, not an ElementHandle
863+ * - Supports multiple selectors with "any" mode
864+ *
865+ * @author Jelle De Loecker <jelle@elevenways.be>
866+ * @since 1.4.1
867+ * @version 1.4.1
868+ *
869+ * @param {string|string[] } selector CSS selector(s) to wait for
870+ * @param {Object } options
871+ * @param {number } options.timeout Max wait time in ms (default: 5000)
872+ * @param {boolean } options.visible Wait for element to be visible (default: false)
873+ *
874+ * @return {Promise<Object|false> } Element info or false if timeout
875+ */
876+ BrowserHelper . setMethod ( async function waitForElement ( selector , options = { } ) {
877+
878+ const timeout = options . timeout || 5000 ;
879+ const visible = options . visible || false ;
880+
881+ if ( ! this . page ) {
882+ throw new Error ( 'Browser not loaded. Call load() or goto() first.' ) ;
883+ }
884+
885+ const selectors = Array . isArray ( selector ) ? selector : [ selector ] ;
886+
887+ return this . evaluate ( ( selectors , timeout , visible ) => {
888+ return new Promise ( ( resolve ) => {
889+
890+ const isVisible = ( el ) => {
891+ if ( ! visible ) return true ;
892+ const rect = el . getBoundingClientRect ( ) ;
893+ return rect . width > 0 && rect . height > 0 ;
894+ } ;
895+
896+ const findElement = ( ) => {
897+ for ( let selector of selectors ) {
898+ const el = document . querySelector ( selector ) ;
899+ if ( el && isVisible ( el ) ) {
900+ return el ;
901+ }
902+ }
903+ return null ;
904+ } ;
905+
906+ const createResult = ( el ) => ( {
907+ found : true ,
908+ selector : selectors . find ( s => document . querySelector ( s ) ) ,
909+ tagName : el . tagName . toLowerCase ( ) ,
910+ text : el . textContent
911+ } ) ;
912+
913+ const found = findElement ( ) ;
914+ if ( found ) {
915+ return resolve ( createResult ( found ) ) ;
916+ }
917+
918+ const observer = new MutationObserver ( ( ) => {
919+ const el = findElement ( ) ;
920+ if ( el ) {
921+ observer . disconnect ( ) ;
922+ resolve ( createResult ( el ) ) ;
923+ }
924+ } ) ;
925+
926+ observer . observe ( document . body , {
927+ childList : true ,
928+ subtree : true ,
929+ attributes : visible
930+ } ) ;
931+
932+ setTimeout ( ( ) => {
933+ observer . disconnect ( ) ;
934+ const el = findElement ( ) ;
935+ resolve ( el ? createResult ( el ) : false ) ;
936+ } , timeout ) ;
937+ } ) ;
938+ } , selectors , timeout , visible ) ;
939+ } ) ;
940+
941+ /**
942+ * Wait for all pending AJAX/fetch requests to complete.
943+ * This is useful after clicking buttons that trigger API calls.
944+ *
945+ * Uses the browser's performance API to track pending requests.
946+ *
947+ * @author Jelle De Loecker <jelle@elevenways.be>
948+ * @since 1.4.1
949+ * @version 1.4.1
950+ *
951+ * @param {number } timeout Max wait time in ms (default: 5000)
952+ * @param {number } settle_time Time with no requests to consider settled (default: 200)
953+ *
954+ * @return {Promise<boolean> }
955+ */
956+ BrowserHelper . setMethod ( async function waitForAjaxComplete ( timeout = 5000 , settle_time = 200 ) {
957+
958+ if ( ! this . page ) {
959+ throw new Error ( 'Browser not loaded. Call load() or goto() first.' ) ;
960+ }
961+
962+ return this . evaluate ( ( timeout , settle_time ) => {
963+ return new Promise ( ( resolve ) => {
964+ const start = Date . now ( ) ;
965+ let lastActivity = Date . now ( ) ;
966+
967+ const checkSettled = ( ) => {
968+ const now = Date . now ( ) ;
969+
970+ const entries = performance . getEntriesByType ( 'resource' ) ;
971+ const pending = entries . filter ( e => {
972+ return e . startTime > ( performance . now ( ) - settle_time ) &&
973+ ( e . initiatorType === 'fetch' || e . initiatorType === 'xmlhttprequest' ) ;
974+ } ) ;
975+
976+ if ( pending . length > 0 ) {
977+ lastActivity = now ;
978+ }
979+
980+ if ( now - lastActivity >= settle_time ) {
981+ return resolve ( true ) ;
982+ }
983+
984+ if ( now - start > timeout ) {
985+ return resolve ( false ) ;
986+ }
987+
988+ setTimeout ( checkSettled , 50 ) ;
989+ } ;
990+
991+ checkSettled ( ) ;
992+ } ) ;
993+ } , timeout , settle_time ) ;
994+ } ) ;
995+
996+ /**
997+ * Wait for client-side navigation to complete.
998+ * This handles both full page navigations and Hawkejs SPA-style navigations.
999+ *
1000+ * For Hawkejs navigations, it waits for:
1001+ * - The scene to finish rendering
1002+ * - Any pending AJAX requests to settle
1003+ *
1004+ * @author Jelle De Loecker <jelle@elevenways.be>
1005+ * @since 1.4.1
1006+ * @version 1.4.1
1007+ *
1008+ * @param {number } timeout Max wait time in ms (default: 5000)
1009+ *
1010+ * @return {Promise<boolean> }
1011+ */
1012+ BrowserHelper . setMethod ( async function waitForClientNavigation ( timeout = 5000 ) {
1013+
1014+ if ( ! this . page ) {
1015+ throw new Error ( 'Browser not loaded. Call load() or goto() first.' ) ;
1016+ }
1017+
1018+ return this . evaluate ( ( timeout ) => {
1019+ return new Promise ( ( resolve ) => {
1020+ const start = Date . now ( ) ;
1021+
1022+ const checkReady = ( ) => {
1023+ const now = Date . now ( ) ;
1024+
1025+ if ( now - start > timeout ) {
1026+ return resolve ( false ) ;
1027+ }
1028+
1029+ if ( typeof hawkejs !== 'undefined' && hawkejs . scene ) {
1030+ const renderer = hawkejs . scene . general_renderer ;
1031+
1032+ if ( renderer && ! renderer . rendering && document . readyState === 'complete' ) {
1033+ setTimeout ( ( ) => resolve ( true ) , 100 ) ;
1034+ return ;
1035+ }
1036+ } else if ( document . readyState === 'complete' ) {
1037+ setTimeout ( ( ) => resolve ( true ) , 100 ) ;
1038+ return ;
1039+ }
1040+
1041+ setTimeout ( checkReady , 50 ) ;
1042+ } ;
1043+
1044+ checkReady ( ) ;
1045+ } ) ;
1046+ } , timeout ) ;
1047+ } ) ;
1048+
8011049// Export the BrowserHelper class
8021050module . exports = BrowserHelper ;
0 commit comments