diff --git a/examples/page-change-evt.rkt b/examples/page-change-evt.rkt new file mode 100644 index 0000000..c1314df --- /dev/null +++ b/examples/page-change-evt.rkt @@ -0,0 +1,60 @@ +#lang racket/base + +(require marionette + web-server/servlet + (only-in xml current-unescaped-tags html-unescaped-tags)) + +(current-unescaped-tags html-unescaped-tags) + +(define ((app n) _req) + (send/suspend/dispatch + (lambda (embed/url) + (sleep 1) + (response/xexpr + #:preamble #"" + `(html + (head + (script ([src "https://cdn.jsdelivr.net/npm/unpoly@3.10.2/unpoly.min.js"]))) + (body + (div.content + (p ,(format "Counter: ~a" n)) + (a + ([class "continue-button"] + [href ,(embed/url (app (add1 n)))] + [up-target ".content"]) + "Continue")))))))) + +(define (run-marionette port) + (call-with-marionette/browser/page! + #:headless? #f + (lambda (p) + (page-goto! p (format "http://127.0.0.1:~a" port)) + (define e (page-change-evt p)) + (element-click! (page-wait-for! p ".continue-button")) + (println (page-url p)) + (println `(sync-result ,(sync e))) + (println (page-url p))))) + +(module+ main + (require racket/async-channel + web-server/servlet-dispatch + web-server/web-server) + + (define port-or-exn-ch + (make-async-channel)) + (define stop + (serve + #:confirmation-channel port-or-exn-ch + #:dispatch (dispatch/servlet (app 1)) + #:port 0)) + (define port-or-exn + (sync port-or-exn-ch)) + (when (exn:fail? port-or-exn) + (raise port-or-exn)) + (define marionette-thd + (thread + (lambda () + (run-marionette port-or-exn)))) + (with-handlers ([exn:break? void]) + (sync/enable-break marionette-thd)) + (stop)) diff --git a/marionette-doc/scribblings/marionette.scrbl b/marionette-doc/scribblings/marionette.scrbl index 3a23c5b..c98df12 100644 --- a/marionette-doc/scribblings/marionette.scrbl +++ b/marionette-doc/scribblings/marionette.scrbl @@ -382,6 +382,16 @@ separately. the entire page is captured. } +@defproc[(page-change-evt [page page?]) (evt/c void?)]{ + Returns a synchronizable event that becomes ready for synchronization + when the page contents have changed (for example, when user navigates + to another page). The synchronization result of a page change event is + @racket[void]. Once a page change event has synchronized, a new event + must be created in order to observe new page changes. + + @history[#:added "1.4"] +} + @subsection[#:tag "reference/element"]{Element} diff --git a/marionette-lib/info.rkt b/marionette-lib/info.rkt index 221cf05..0ee70f8 100644 --- a/marionette-lib/info.rkt +++ b/marionette-lib/info.rkt @@ -1,7 +1,7 @@ #lang info (define license 'BSD-3-Clause) -(define version "1.3.1") +(define version "1.4") (define collection "marionette") (define deps '("base" diff --git a/marionette-lib/page.rkt b/marionette-lib/page.rkt index 6f8af71..e2973f3 100644 --- a/marionette-lib/page.rkt +++ b/marionette-lib/page.rkt @@ -1,10 +1,13 @@ #lang racket/base -(require json +(require file/sha1 + json net/base64 net/url racket/contract/base racket/match + racket/promise + racket/random racket/string "private/browser.rkt" "private/json.rkt" @@ -130,7 +133,10 @@ (with-page p (sync (handle-evt - (marionette-execute-async-script! (page-marionette p) (wrap-async-script s) args) + (marionette-execute-async-script! + (page-marionette p) + (wrap-async-script s) + args) (λ (res) (match (hash-ref res 'value) [(hash-table ('error (js-null)) @@ -303,6 +309,26 @@ res-value/decode))))) +;; page-change-evt ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(provide + page-change-evt) + +(define (page-change-evt p) + (define token (bytes->hex-string (crypto-random-bytes 32))) + (page-execute! p (template "support/set-page-change-token.js") token) + (log-marionette-debug "set PageChangeToken=~s" token) + (handle-evt + (delay/thread + (let loop () + (define current (page-execute! p (template "support/get-page-change-token.js"))) + (log-marionette-debug "get PageChangeToken=~a" current) + (when (equal? current token) + (sleep 0.1) + (loop)))) + void)) + + ;; element ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (provide diff --git a/marionette-lib/support/get-page-change-token.js b/marionette-lib/support/get-page-change-token.js new file mode 100644 index 0000000..313fb3c --- /dev/null +++ b/marionette-lib/support/get-page-change-token.js @@ -0,0 +1 @@ +return (window.$$marionette || {}).PageChangeToken; diff --git a/marionette-lib/support/set-page-change-token.js b/marionette-lib/support/set-page-change-token.js new file mode 100644 index 0000000..cd4e9c2 --- /dev/null +++ b/marionette-lib/support/set-page-change-token.js @@ -0,0 +1,16 @@ +const Marionette = window.$$marionette || {}; +if (Marionette.PageChangeToken === undefined) { + Marionette.PageChangeToken = arguments[0]; + if (Marionette.patchedHistory === undefined) { + Marionette.patchedHistory = true; + for (const method of ["pushState", "replaceState"]) { + window.history[method] = new Proxy(window.history[method], { + apply: (target, self, args) => { + delete Marionette["PageChangeToken"]; + return target.apply(self, args); + }, + }); + } + } +} +window.$$marionette = Marionette;