TL;DR
Need to wait for a page to show a specific string or pattern (e.g. in pages tailing some logs) and want to be notified, so you can go about other businesses while waiting? Use the JS Notification API along with bookmarklets to do the trick!
Motivation
As a developer in 2021, I spend a lot of time staring at console outputs from CI/CD pipelines, or logs tabs from kubernetes-like clusters, to either wait for some state or to catch any exception or weirdness.
This is tiresome, especially since I usually know what to expect. I would like to have some sort of notification mechanism, so I can go around other businesses and only switch my attention back when the awaited condition is met.
Let me give an example with a hypothetical Jenkins prompt.
Let's say a prompt was defined in Jenkins (with a default timeout of 1 minute), to ask whether or not to save the test output after e2e tests finished. If the build and e2e tests take an average of 30 minutes, it is very hard not to miss the prompt window, and you will end up restarting the pipeline often and hope this time you won't forget to monitor the test's progress. Well, this is exactly the kind of thing I faced over and over.
There are lots of extensions out there to get notified when something appears on a page, but they are designed to check/refresh the page at a regular interval (and in the background). Moreover, the configuration is often complex and tiresome, especially if the pattern you are looking for changes every time. Those are well suited for stock values or changing prices on Amazon, but not for my use case.
What I want
Given a page that progressively shows text (e.g. pages tailing some logs, Jenkins Console Output page, ...), I want to get a notification as soon as a given text or pattern appears.
This should work at least on Mac, preferably on Chrome, but another browser would work as well. Ideally, the solution would work on all OS/browsers.
About browser notifications
The notification API
The Notification API is what is used under the hood by all sites showing notifications. You should be familiar with the all-too-annoying prompt:
The API is simple enough. You first ask for permission to show notifications (triggering the prompt above the first time), then just create a Notification
object:
// In your favorite dev tools console (Chrome, Firefox, ...)
// Note: run it on a page you trust, as you will have to allow notification from this origin
Notification.requestPermission((permission) => {
if (permission !== 'denied') {
new Notification("test");
}
})
This can be executed directly from the devtools, or a javascript loaded on a page.
Note that if you are using Firefox, a change in version 72 blocks the popup when the permission request is not triggered by a user interaction. Instead, a small icon will wiggle at the right of the URL bar. If you still really need the popup, there are still some nasty workarounds.
Permissions and OS/Browser support for notifications
The Notification API is supported in all major browsers for a while now.
For a notification to show, two kinds of permissions must be granted:
- browser permission: your OS needs to allow permissions from your browser. See this article for how to do it (explaining the process for Chrome, but would be the same for other browsers).
- website permission: even when triggering the notification from the devtools, the notification will be attached to the page currently displayed. The latter thus needs to have show notifications granted. See Enabling and Disabling Browser Notifications in Various Browsers
Still no notifications? Check out those resources
⚠️ |
---|
For some reason, notifications do not work anymore since Chrome 92.0.4515.159 on my laptops (MAC OSX Catalina // Big Sur). Fortunately, Firefox and Safari are still working fine. |
Writing the JS
With the theory in place, I need a JS code that:
- waits for a given text to appear on the page (a basic search and a sleep in a loop will do the trick);
- shows a persistent notification using the Notification API once the text is found;
- cherry on the cake: configures the notification so that we switch automatically to the right browser tab on click.
Here is a code doing exactly that (supporting regular expressions as well as text):
To use it, open the devtools console on any page (⌘
+⌥
+i
on Mac), copy-paste it and then call waitForText
with whatever pattern(s) you want to wait for. Here are some examples:
// be notified when an exact string appears
waitForText("BUILD SUCCESS")
// be notified when any of multiple strings appears
waitForText(["BUILD SUCCESS", "BUILD FAILURE"])
// look for a pattern
waitForText(/BUILD (SUCCESS|FAILURE)/)
// look for patterns or strings
waitForText([/hello.*world/, "foo"])
// look for string only in part of the page
waitForText("hello", document.getElementById("out"))
// customise the notification
waitForText("world", undefined, {title: "hello"});
Making the script easy to use
The above script works well, but I don't want to have to copy-paste it to the devtools console each time I need it. Fortunately, Bookmarklets can save the day!
A bookmarklet is a bookmark stored in a web browser that contains JavaScript commands that add new features to the browser.
I used two tools to generate the bookmarklet URL:
- https://jscompress.com/ to minify my script (check ECMAScript 2021),
- https://mcdlr.com/js-inject/ to generate the bookmarklet link
The two steps above give us the following URL, that you can add as a bookmark (new bookmark, then paste the text below in the URL field):
javascript:(function(){function sleep(a){return new Promise(b=>setTimeout(b,a))}function toArrayOfPatterns(a){return(Array.isArray(a)?a:[a]).map(a=>%22string%22==typeof a?new RegExp(a):a)}async function isNotificationPermissionGranted(){return new Promise(a=>{%22granted%22===Notification.permission?a(!0):%22default%22===Notification.permission?(console.log(%22Requesting notification permission.%22),console.log(%22%25cFROM FIREFOX 72 onwards%25c\nIt is not possible to trigger the permission prompt without user interaction. A little icon should have appeared on the left of the URL bar though. Click on it to grant permission !\nSee https://blog.mozilla.org/futurereleases/2019/11/04/restricting-notification-permission-prompts-in-firefox/%22,%22font-weight: bold; color: red%22,%22%22),Notification.requestPermission().then(()=>{%22granted%22===Notification.permission?a(!0):a(!1)})):((()=>console.log(%22%25cYou didn%27t grant notification permission for this page. Aborting.%22,%22font-weight: bold; color: red%22))(),a(!1))})}async function waitForText(a,b,c){if(!(await isNotificationPermissionGranted()))return;const d=b??document.body,e=toArrayOfPatterns(a);for(console.log(`Waiting for any of ${e}.`,%22Type %27window.stopWaiting = true%27 on the developer console to stop the wait, or reload the page.%22);!window.stopWaiting;){console.log(%22...%22);let a=e.find(a=>a.test(d.innerHTML));if(a){const b=a.exec(d.innerHTML)[0];console.log(`${b} found. Showing notification.`);let e=Object.assign({title:%22Found!%22,body:`found %22${b}%22 in ${document.title}`,requireInteraction:!0},c),f=new Notification(e.title,e);return void(f.onclick=function(){window.parent.parent.focus(),f.close()})}await sleep(5e3)}console.log(%22WaitForText aborted.%22),window.stopWaiting=!1}window.stopWaiting=!1,window.waitForText=waitForText;})();
With this new bookmarklet, the process is now:
- open a page,
- click on the bookmark,
- open the console,
- call
waitForText(...)
.
Done!
Going further
The script in this article is very generic. If you always need to wait for the same pattern(s), you can easily add some exports at the end of the script and re-generate the bookmarklet. For example:
// at the end of the script
window.waitForEndOfBuild = async function() {
await waitForText(/BUILD (SUCCESS|FAILURE)/);
};
// ... more functions ...
If you do not want to open the console each time, you can also call waitForText
directly in the script, thus triggering the wait as soon as you click the bookmarklet:
// at the end of the script
await waitForText(/BUILD (SUCCESS|FAILURE)/);
The possibilities are endless.
Written with ❤ by derlin