Check connection loss in JavaScript on the browser [duplicate] - javascript

This question already has answers here:
navigator.onLine not always working
(2 answers)
Closed last month.
I referred to this Detect the Internet connection is offline? question to find out how to check if the browser is offline or online.
So I used window.navigator.onLine to find that out.
The problem is that, no matter what I do, window.navigator.onLine is always true.
I am using brave browser, but I'm not sure if that's related to the issue, it's chromium based.
I'm on Ubuntu Linux, desktop.
I just want to detect when the browser becomes offline to show a small message "connection lost".
In react the code looks as follows:
const online = window.navigator.onLine
useEffect(() => {
if (online) return
console.log("Connection lost!")
}, [online])
try to toggle your wifi on and on to see the console logs
Here's a stack blitz instance to try it out, it's a pretty small code, (Click me)

The property sends updates whenever the browser's ability to connect to the network changes. The update occurs when the user follows links or when a script requests a remote page.
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/onLine
So the value won't update unless you make a request of some sort.
There are also some implementation specific notes on the same url.
In Chrome and Safari, if the browser is not able to connect to a local area network (LAN) or a router, it is offline; all other conditions return true.
In other words, if there is any sort of network access it will be true, even if you are not connected to the internet.
So the best way to check this is probably to just make a request to an API endpoint or other resource that is only available while online and base your status on if the request was successful or not. Since in most cases just being "online" isn't worth much if your API is inaccessible this would probably provide better information to your users as well.

Need to use the event listener for this: window.addEventListener('online', () => { ...});.
Inside the callback for listener, do setState to check online off-line toggle.
here is small hook i created in reactjs to handle online offline states:
import { useEffect, useState } from 'react';
// toastr alert messages
import { showOffline, showOnline } from 'utils/alerts';
const useNetworkStatus = () => {
const [state, setState] = useState(true);
async function isOnline() {
// if its offline and to check if window?.navigator is supported.
if (!window?.navigator?.onLine) {
setState(false);
return false;
}
// Failover case:
// navigator.onLine cannot be trusted: there's situation where you appear to be online (connect to a network with no internet)
// but still cannot access the internet.
// So to fix: we request to our own origin to avoid CORS errors
const url = new URL(window.location.origin);
// with random value to prevent cached responses
url.searchParams.set('rand', Date.now());
try {
const response = await fetch(url.toString(), { method: 'HEAD' });
setState(true);
return response.ok;
} catch {
setState(false);
return false;
}
}
useEffect(() => {
const setOnlineOnVisibleChange = async () => {
// if its page is visible and state was offline
if (!document?.hidden && !state) {
if (await isOnline()) showOnline();
}
};
// on visiting the page again if the state is offline and network is online, then show online alert
if ('hidden' in document)
document.addEventListener('visibilitychange', setOnlineOnVisibleChange, false);
return () => document.removeEventListener('visibilitychange', setOnlineOnVisibleChange, false);
}, [state]);
useEffect(() => {
async function changeStatus() {
if (await isOnline()) showOnline();
else showOffline();
}
// Listen for the page to be finished loading
window.addEventListener('load', () => {
// if its offline
if (!isOnline()) showOffline();
});
window.addEventListener('online', changeStatus);
window.addEventListener('offline', changeStatus);
return () => {
window.removeEventListener('online', changeStatus);
window.removeEventListener('offline', changeStatus);
};
}, []);
};
export default useNetworkStatus;

Related

Does anyone know how to define navigator online in main process in electron?

I know you can use navigator onLine inside the renderer process because it's a rendered inside a browser. But what I'm trying to do is something like this in the main process:
if (navigator.onLine){
mainWindow.loadURL("https://google.com")
} else {
mainWindow.loadFile(path.join(__dirname, 'index.html'));
}
So basically if the user is offline, just load a local html file, and if they're online, take them to a webpage. But, like expected, I keep getting the error that 'navigator is not defined'. Does anyone know how can I somehow import the navigate cdn in the main process? Thanks!
TL;DR: The easiest thing to do is to just ask Electron. You can do this via the net module from within the Main Process:
const { net } = require ("electron");
const isInternetAvailable = () => return net.isOnline ();
// To check:
if (isInternetAvailable ()) { /* do something... */ }
See Electron's documentation on the method; specifically, this approach doesn't tell you whether your service is accessible via the internet, but rather that a service can be contacted (or not even this, as the documentation mentions links which would not involve any HTTP request at all).
However, this is not a reliable measurement and you might want to increase its hit rate by manuallly checking whether a certain connection can be made.
In order to check whether an internet connection is available, you'll have to make a connection yourself and see if it fails. This can be done from the Main Process using plain NodeJS:
// HTTP code basically from the NodeJS HTTP tutorial at
// https://nodejs.dev/learn/making-http-requests-with-nodejs/
const https = require('https');
const REMOTE_HOST = "google.com"; // Or your domain
const REMOTE_EP = "/"; // Or your endpoint
const REMOTE_PAGE = "https://" + REMOTE_HOST + REMOTE_EP;
function checkInternetAvailability () {
return new Promise ((resolve, reject) => {
const options = {
hostname: REMOTE_HOST,
port: 443,
path: REMOTE_EP,
method: 'GET',
};
// Try to fetch the given page
const req = https.request (options, res => {
// Yup, that worked. Tell the depending code.
resolve (true);
req.destroy (); // This is no longer needed.
});
req.on ('error', error => {
reject (error);
});
req.on ('timeout', () => {
// No, connection timed out.
resolve (false);
req.destroy ();
});
req.end ();
});
}
// ... Your window initialisation code ...
checkInternetAvailability ().then (
internetAvailable => {
if (internetAvailable) mainWindow.loadURL (REMOTE_PAGE);
else mainWindow.loadFile (path.join (__dirname, 'index.html'));
// Call any code needed to be executed after this here!
}
).catch (error => {
console.error ("Oops, couldn't initialise!", error);
app.quit (1);
});
Please note that this code here might not be the most desirable since it just "crashes" your app with exit code 1 if there is any error other than connection timeout.
This, however, makes your startup asynchronous, which means that you need to pay attention on the execution chain of your app startup. Also, startup may be really slow in case the timeout is reached, it may be worth considering NodeJS' http module documentation.
Also, it makes sense to actually try to retrieve the page you're wanting to load in the BrowserWindow (constant values REMOTE_HOST and REMOTE_EP), because that also gives you an indication whether your server is up or not, although that means that the page will be fetched twice (in the best case, when the connection test succeeds and when Electron loads the page into the window). However, that should not be that big of a problem, since no external assets (images, CSS, JS) will be loaded.
One last note: This is not a good metric of whether any internet connection is available, it just tells you whether your server answered within the timeout window. It might very well be that any other service works or that the connection just is very slow (i.e., expect false negatives). Should be "good enough" for your use-case though.

Unable to load a specific URL with Cypress

I’m unable to load the following URL with Cypress. Getting timeout error. I have set the page load time to 2 mins, still same issue. General URLs eg. (https://www.google.co.nz/) works fine.
it(‘First Test’, () => {
cy.visit(‘https://shop.countdown.co.nz/‘)
})
Here's a way, not the best, could be improved...
The Countdown site has an aversion to being run in an iframe, but it can be tested in a child window, see custom command here Cypress using child window
Cypress.Commands.add('openWindow', (url, features) => {
const w = Cypress.config('viewportWidth')
const h = Cypress.config('viewportHeight')
if (!features) {
features = `width=${w}, height=${h}`
}
console.log('openWindow %s "%s"', url, features)
return new Promise(resolve => {
if (window.top.aut) {
console.log('window exists already')
window.top.aut.close()
}
// https://developer.mozilla.org/en-US/docs/Web/API/Window/open
window.top.aut = window.top.open(url, 'aut', features)
// letting page enough time to load and set "document.domain = localhost"
// so we can access it
setTimeout(() => {
cy.state('document', window.top.aut.document)
cy.state('window', window.top.aut)
resolve()
}, 10000)
})
})
Can test with that like this
cy.openWindow('https://shop.countdown.co.nz/').then(() => {
cy.contains('Recipes').click()
cy.contains('Saved Recipes', {timeout:10000}) // if this is there, have navigated
})
I bumped the setTimeout() in custom command to 10 seconds, cause this site drags it's feet a bit.
Configuration:
// cypress.json
{
"baseUrl": "https://shop.countdown.co.nz/",
"chromeWebSecurity": false,
"defaultCommandTimeout": 20000 // see below for better way
}
Command timeout error
Using Gleb's child window command, there's a timeout error that I can't track the source of.
To avoid it I set "defaultCommandTimeout": 20000 in config, but since it's only needed for the openWindow call it's better to remove the global setting and use this instead
cy.then({timeout:20000}, () => {
cy.openWindow('https://shop.countdown.co.nz/', {}).then(() => {
cy.contains('Recipes').click()
cy.contains('Saved Recipes', {timeout:10000}) // if this is there, have navigated
})
})
To check if the long command timeout only applies once, break one of the inner test commands and check that that it times out in the standard 4000 ms.
cy.then({timeout:20000}, () => {
cy.openWindow('https://shop.countdown.co.nz/', {}).then(() => {
cy.contains('Will not find this').click() // Timed out retrying after 4000ms
The quotes are wrong. Try the below code:
it('First Test', ()=>{ cy.visit('https://shop.countdown.co.nz/') })
On trying to visit the URL I am getting the error:
cy.visit() failed trying to load:
https://shop.countdown.co.nz/
We attempted to make an http request to this URL but the request
failed without a response.
We received this error at the network level:
Error: ESOCKETTIMEDOUT
Common situations why this would fail:
you don't have internet access
you forgot to run / boot your web server
your web server isn't accessible
you have weird network configuration settings on your computer
Error Screenshot:
Lets look into the common situations where this might happen:
you don't have internet access: I have a internet access, so this can be ruled out.
you forgot to run / boot your web server - Your site is accessible from a normal browser, this can be ruled out as well.
your web server isn't accessible - This is a possibility where may be there are firewall settings at the server end because of which cypress is not getting any response when accessing the site.
you have weird network configuration settings on your computer - This can be ruled out as well.
I had a similar issue, so what I observed in my case was that the URL was not getting added to the iframe src property and hence cy.visit() was getting timed out each time.
So, I added the URL manually to the src property of the iframe.
Here's my custom command for reference:
Cypress.Commands.add('goto', url => {
return new Promise(res => {
setTimeout(() => {
const frame = window.top.document.getElementsByClassName('aut-iframe')[0];
frame.src = url;
var evt = window.top.document.createEvent('Event');
evt.initEvent('load', false, false);
window.dispatchEvent(evt);
res();
}, 300);
});
});
Now use cy.goto('https://yoururl.com') and you are good to go.

Is there a way to get Chrome to “forget” a device to test navigator.usb.requestDevice, navigator.serial.requestPort?

I'm hoping to migrate from using WebUSB to SerialAPI (which is explained nicely here).
Current Code:
try {
let device = await navigator.usb.requestDevice({
filters: [{
usbVendorId: RECEIVER_VENDOR_ID
}]
})
this.connect(device)
} catch (error) {
console.log(DEVICE_NAME + ': Permission Denied')
}
New Code:
try {
let device = await navigator.serial.requestPort({
filters: [{
usbVendorId: RECEIVER_VENDOR_ID
}]
})
this.connect(device)
} catch (error) {
console.log(DEVICE_NAME + ': Permission Denied')
}
The new code appears to work, but I think it's because the browser has already requested the device via the old code.
I've tried restarting Chrome as well as clearing all of the browsing history. Even closed the USB-claiming page and claimed the device with another app (during which it returns the DOMException: Unable to claim interface error), but Chrome doesn't seem to want to ask again. It just happily streams the data with the previous connection.
My hope was that using SerialAPI would be a way to avoid fighting over the USB with other processes, or at least losing to them.
Update
I had forgotten about:
Failed to execute 'requestPort' on 'Serial': "Must be handling a user gesture to show a permission request"
Does this mean that the user will need to use a button to connect to the device via SerialUSB? I think with WebUSB I was able to make the connect window automatically pop up.
For both APIs, as is noted in the update, a user gesture is required in order to call the requestDevice() or requestPort() method. It is not possible to automatically pop up this prompt. (If there is that's a bug so please let the Chrome team know so we can fix it.)
Permissions granted to a site through the WebUSB API and Web Serial API are currently tracked separately so permission to access a device through one will not automatically translate into the other.
There is not currently a way to programatically forget a device permission. That would require the navigator.permissions.revoke() method which has been abandoned. You can however manually revoke permission to access the device by clicking on the "lock" icon in the address bar while visiting the site or going to chrome://settings/content/usbDevices (for USB devices) and chrome://settings/content/serialPorts (for serial ports).
To get Chrome to "forget" the WebUSB device previously paired via navigator.usb.requestDevice API:
Open the page paired to the device you want to forget
Click on the icon in the address bar
Click x next to device. If nothing is listed, then there are no paired devices for this web page.
The new code was NOT working. It just appeared to be because Chrome was already paired with the port via the old code. There is no way the "new code" could have worked because, as noted in Kalido's comment, the SerialAPI (due to its power) requires a user gesture to connect.
The code I'm using to actually connect and get data is basically built up from a few pieces from the above links in the OP:
navigator.serial.addEventListener('connect', e => {
// Add |e.target| to the UI or automatically connect.
console.log("connected");
});
navigator.serial.addEventListener('disconnect', e => {
// Remove |e.target| from the UI. If the device was open the disconnection can
// also be observed as a stream error.
console.log("disconnected");
});
console.log(navigator.serial);
document.addEventListener('DOMContentLoaded', async () => {
const connectButton = document.querySelector('#connect') as HTMLInputElement;
if (connectButton) {
connectButton.addEventListener('click', async () => {
try {
// Request Keiser Receiver from the user.
const port = await navigator.serial.requestPort({
filters: [{ usbVendorId: 0x2341, usbProductId: not_required }]
});
try {
// Open and begin reading.
await port.open({ baudRate: 115200 });
} catch (e) {
console.log(e);
}
while (port.readable) {
const reader = port.readable.getReader();
try {
while (true) {
const { value, done } = await reader.read();
if (done) {
// Allow the serial port to be closed later.
reader.releaseLock();
break;
}
if (value) {
console.log(value);
}
}
} catch (error) {
// TODO: Handle non-fatal read error.
console.log(error);
}
}
} catch (e) {
console.log("Permission to access a device was denied implicitly or explicitly by the user.");
console.log(e);
console.log(port);
}
}
}
The device-specific Vendor and Product IDs would obviously change from device to device. In the above example I have inserted an Arduino vendor ID.
It doesn't answer the question of how to get Chrome to "forget", but I'm not sure if that's relevant when using SerialAPI because of the required gesture.
Hopefully someone with more experience will be able to post a more informative answer.

Service worker: caching cross-domain response with all static assets as well

I am currently working on a proof-of-concept for a project but I just can't get my head around
documentation
and implementation :)
The case is as follows:
I have my main app (React) that has a list of links. All of them link to a specific page.
These links open up in an iframe.
That's all basically.
So my app runs on "app.domain.com" and the forms urls are like "pages.domain.com/pages/pageA.html" etc.
What I need to do in this poc is to make these pages offline available, including(!) the assets for this pages (css/js/img)
I already created a simple service worker.
const CACHE_NAME = "poc-forms";
self.addEventListener("install", (event) => {
console.log("sw installing…");
});
self.addEventListener("activate", (event) => {
console.log("sw now ready to handle fetches");
event.waitUntil(caches.open(CACHE_NAME).then(() => self.clients.claim()));
});
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url);
if (url.pathname.includes("forms")) {
event.respondWith(
(async function () {
var cache = await caches.open(CACHE_NAME);
var cachedFiles = await cache.match(event.request);
if (cachedFiles) {
return cachedFiles;
} else {
try {
var response = await fetch(event.request);
await cache.put(event.request, response.clone());
return response;
} catch (e) {
console.log(" something when wrong!");
}
}
})()
);
}
});
It fetches the request and checks if it's already in the cache or not.
If it's not, cache it.
This works.
But where I'm stuck:
how can I also store the css and js that are needed for the pages as well? Do I need find a way to get all links and loop over them, fetch them and store them?
I heard about Google Workbox, also went through the documentation but it's just not clear to me, like how to transform my current SW into something that works with workbox, with the whole registerRoute-thing on a fetch...
the service worker will only capture the fetches when the page is refreshed. The clients.claim() should fix this, but it doesn't actually...
If someone out there could help me out with this, much appreciated!
thanks,
Mario
The Service Worker APIs do not allow for a service worker registered on app.domain.com to control either navigation requests or subresource requests associated with an <iframe> that is hosted on pages.domain.com. That's not something that Workbox can help with—it's just not possible.
From the perspective of the service worker, an <iframe> with a different origin than the host page is equivalent to a new window or tab with a different origin. The service worker can't "see" or take control of it.
Also, this might help others, small variant of what I want to accomplish:
Why isn't my offline cached subdomain page not loading when called from other subdomain?
Ok, we ended up using a different approach, because the app we are loading inside the iframe exposed an api apparently, where we can hook into.
To cache the entire page, with all the assets, this was the code that worked for me:
importScripts(
"https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js"
);
const { skipWaiting, clientsClaim } = workbox.core;
const { registerRoute } = workbox.routing;
const { StaleWhileRevalidate } = workbox.strategies;
skipWaiting();
clientsClaim();
registerRoute(
({ request }) => {
console.log(" request ", request.destination);
return (
request.destination === "iframe" ||
request.destination === "document" ||
request.destination === "image" ||
request.destination === "script" ||
request.destination === "style" ||
request.destination === "font"
);
},
new StaleWhileRevalidate()
);

How to use cookie obtained in BrowserWindow within webview

I have searched all available information on this and the documentation is not helping me.
I have an Electron app (React internals) where a web page is to be shown in a webview.
The domain e.g. https://root.domain.com stays the same but the rest of the URL will be different depending on props passed to the component showing the webview.
I found some code here linked from a Youtube video from BuildingXwithJS on testing which will use a BrowserWindow to allow a user login, then save the authenticated cookie for use within the app.
auth() {
const remote = electron.remote;
// Get a partitioned session to use with webview
const ses = remote.session.fromPartition("myPartition");
// create new electron browser window
const BrowserWindow = remote.BrowserWindow;
let win = new BrowserWindow({width: 800, height: 600});
// cleanup on close
win.on('closed', () => {
win = null;
});
// wait for page to finish loading
win.webContents.on('did-finish-load', () => {
// if auth was succesful
if (win.webContents.getURL() === 'https://root.domain.com/Home/') {
// get all cookies
win.webContents.session.cookies.get({}, async (error, cookies) => {
if (error) {
console.error('Error getting cookies:', error);
return;
}
// store cookies
cookies.forEach(c => {
if(c.domain.includes('root.domain.com')){
ses.cookies.set(c,(e) => e? console.log("Failed: %s",e.message):null})
}
// close window
win.close();
});
}
});
// Load login page
win.loadURL(loginURL);
}
Now when I view a page in webview in a separate component like this:
<webview
ref={a => (this.view = a)}
partition="myPartition"
src={`https://root.domain.com/${providedURL}`}
style={{
display: "flex",
width: "100%",
height: "100%"
}}
/>
I get the login page.
So I tried to following to set the cookies on the webview when it finishes loading, then reload:
this.view.addEventListener("did-finish-load", e => {
// Get partitioned session
const ses = remote.session.fromPartition("myPartition");
// Loop through cookies and add them to the webview
ses.cookies.get({}, (error, cookies) => {
cookies.forEach(cookie => {
// this.view doesn't return the webview in here so I use the event
e.target.getWebContents().session.cookies.set(cookie,(e)=> e? console.log(e):null);
});
// Reload with cookies
e.target.getWebContents().reload();
});
this.setState({ loading: false, failure: false });
});
But still I get login page every time!
The frustrating thing about this is that the auth() function defined above (in another wrapper component) shows a logged in screen every time it runs after the user logs in once so the login session is active and the cookie is stored somewhere - but where? And how can I make this webview see it? Would I be better with an Iframe ?? My requirement to show an external url inline is a must have for my stakeholders so I need some way to make it work.
I am far from proficient in cookie management and the session management is not clear to me in Electron because there are a number of ways of accessing sessions BrowserWindow.webContents().session orBrowserWindow.webContents().defaultSession or
import { session } from "electron".
Also, when I look at the application tab in devtools, these specific cookies are not showing anywhere (but others are). Why is this?

Categories