Browser history broken when using push state - javascript

I have the following code in my application which tries to retrieve some data from a URL and then update the browser with the new URL.
const response = await fetch(url);
if (response.ok) {
...
window.history.pushState({ loaded: true }, null, url);
}
Now every time I click to re-fetch the data (with a new URL) the URL in the browser is updated successfully. However if I click back it does nothing unless I click enough times for it to go back to the referred page.
My first fix was to change the code above to the following:
const response = await fetch(url);
if (response.ok) {
...
if (window.history.state && window.history.state.loaded)
window.history.pushState({ loaded: true }, null, url);
else
window.history.replaceState({ loaded: true }, null);
}
Now when I click back after it initially loads it goes back to the referred page as I simply replace the state and don't push anything to the history stack, before I would have had to click back twice. However I still have the issue that when I re-fetch the data it will now push to the history stack and nothing happens when I click back.
I thought the easiest thing would be to reload the page when I click back and the data has loaded. Therefore I tried the following:
window.addEventListener('popstate', e => {
if (e.state && e.state.loaded)
window.location.href = window.location.pathname + window.location.search;
});
However now I can't seem to go back to the referred page. This is because even though I reload the page (in the "popstate" event listener) when it fetches the data it says the state has loaded.
I thought this would be null after the reload but since it's not I thought I would force the browser to make sure the initial state is null when it first loads:
window.addEventListener('load', e => {
window.history.replaceState(null, null);
});
However whilst this initially appeared to work I found this was only because I had the developer tools running (with caching disabled).
I'm sure I'm missing something basic as this seems to be quite a common problem. I'd appreciate it if someone could show me what I am doing wrong.

I've found that changing my fetch request to the following seems to work:
await fetch(url, { cache: 'no-store' });
Alternatively adding an addional query string parameter to the fetched url also works.
I'll leave this open for a little bit incase someone has a better solution.

Related

How can I test a client-side redirect to a 3rd party site with Cypress?

Cypress offers a simple way to test for server-side redirects using request:
cy.request({
url: `/dashboard/`,
followRedirect: false, // turn off following redirects
}).then((resp) => {
expect(resp.redirectedToUrl).to.eq('http://example.com/session/new')
})
However this doesn't work for client-side redirects because the page is loaded successfully before the redirect happens, meaning the response is for the page, not for the redirect.
How can I test a client-side redirect?
I need a way of catching the redirect and verifying that:
it occurred
was to the correct URL.
Note:
I don't want to follow the redirect away from the app that is being tested. I'm not testing the whole auth flow. I just need to know there was a redirect.
I can't change this auth flow. The redirect is unavoidable.
The redirect happens during initialisation, not as a result of any user interaction.
The redirect uses: window.location.href = url
See my answer below for an attempt at resolving this.
Update: This isn't solid. I've just done some refactoring and it seems that even this solution is flawed. It is possible for the redirect to happen in between cypress visiting the page and triggering the cy.wait so the test ends up waiting for something that has already happened. The below might still work for you depending on when your redirect is triggered, but if it's triggered on initialisation, it appears this will not work.
Not really loving this solution as the redirect still happens (I haven't found a way to cancel it), but it at least tests that the redirect happens, and allows me to check the query:
cy.intercept({ pathname: `/sessions/new` }).as(`loginRedirect`)
cy.visit(`/dashboard/`)
cy.location().then(($location) => {
cy.wait(`#loginRedirect`).then(($interceptor) => {
const { query } = urlParse($interceptor.request.url)
expect(query).to.equal(`?token=true&redirect=${$location.href}`)
})
})
Note: route2 changed to intercept in v6.
Gleb Bahmutov has an approach at Deal with window.location.replace, which renames window.location in the source to window.__location which is effectively the stub.
It uses cy.intercept() to modify the loading page before it hits the browser, i.e before window.location is instantiated and becomes a totally immutable/incorruptible object.
it('replaces', () => {
cy.on('window:before:load', (win) => {
win.__location = { // set up the stub
replace: cy.stub().as('replace')
}
})
cy.intercept('GET', 'index.html', (req) => { // catch the page as it loads
req.continue(res => {
res.body = res.body.replaceAll(
'window.location.replace', 'window.__location.replace')
})
}).as('index')
cy.visit('index.html')
cy.wait('#index')
cy.contains('h1', 'First page')
cy.get('#replace').should('have.been.calledOnceWith', 'https://www.cypress.io')
})
This stubs replace, but the same should work for the href setter.
Here is what I have found as the only was to detect a successful redirect client side if it is to a third-party website. JavaScript has a handy-dandy function called window.open(). There are many things that you can do with it including redirecting the webpage. This can be done by setting the target to _self or to _top. By using a while loop, you are running code as quickly as you can. Here is a rough-draft of how I would record a client-side redirect.
var redirectURL = 'https://www.example.com/',
redirectWindow = window.open(redirectURL, '_top'),
redirected = false,
count = 0;
while(true) {
if (redirectWindow.document.readyState === 'complete') {
redirected = true;
//Your code here. I am just logging that it is in the process of redirection.
console.log('redirecting')
break;
}
if (count > 2000) {
redirected = false;
break;
}
count++
}
if (redirected == false) {
console.log('Error: Redirect Failed');
}

How to simulate video playing in Netflix without reloading the page?

I'm trying to write a chrome extension where I want to be able to start playing a Netflix video from the main page without having to navigate to the video URL using something like window.location.href = '...', which would cause the page to reload. I want to do this without reloading the page because the content script of a chrome extension is re-injected every time a page reload happens, which I don't want to happen every time.
For example, if you go to the netflix website and click on a movie, the video will start playing without actually doing a full page reload. This suggests that changes to the DOM are triggered via JavaScript? Does anyone know how this is done?
I tried looking at some of the Javascript code to see if there was a function I could call to trigger some sort of event, but the code was too complicated to understand. So another approach I thought of was to do a fetch with the url of the video that I want to play, and then simply replace the contents of the body element with that of the fetched html. For example:
fetch("https://www.netflix.com/watch/70174779")
.then(function(response) {
response.text().then(function(html) {
var parser = new DOMParser();
var doc = parser.parseFromString(html, "text/html");
document.getElementsByTagName(
"body"
)[0].innerHTML = doc.getElementsByTagName("body")[0].innerHTML;
});
})
.catch(function(err) {
console.log("Fetch Error :-S", err);
});
However, when I run this in the Chrome DevTools console, it just results in an empty page. I don't get any helpful errors either. I also tried replacing the entire html contents:
fetch("https://www.netflix.com/watch/70174779")
.then(function(response) {
response.text().then(function(html) {
document.open();
document.write(html);
document.close();
});
})
.catch(function(err) {
console.log("Fetch Error :-S", err);
});
But this also didn't work, and I get a bunch of errors in the console.

javascript window.open infinite loading

I have a JQuery ajax request that can return error status and messages.
To handle them I use this code :
$.ajax("url", {
options: options
}).done(function(text, statusText, data) {
//Manage data
}).fail(function(data) {
//Manage fail
});
In the fail handle I want, in case of 500: internal server error, to open a new tab with the response text (for debug purposes)
I do it this way :
if (data.status === 500) {
var w = window.open("App error", "blank");
w.document.write(data.responseText);
}
And it works !
Except one point : my browser loads the page, the content is displayed (and as it's static content all of this is not a real problem), but the tab is marked as loading... Loading... Loading...
I'm using Firefox 63.0(64bits).
Does anyone know where this comes from ? It's not really annoying, it's just a (fun ?) behavior I don't understand.
Here is a fiddle on which I get the exact same behavior.
It has to do with the w.document.write line. If you close the document, the loader will finish. Change the code to:
if (data.status === 500) {
var w = window.open("App error", "blank");
w.document.write(data.responseText);
w.document.close();
}
Source: Open about:blank window in firefox
The problem is that you are opening a tab without an url, and most browsers expect an url, that's what I do in order to get rid of it:
const newWindow = window.open('#');
if (newWindow) {
newWindow.addEventListener('load', () => {
newWindow.document.open();
newWindow.document.write(request.responseText);
newWindow.document.close();
newWindow.stop();
}, true);
You can replace # with any url path that doesn't exist, the important thing is that once it's loaded, you'll be able to override the content.
It's not the best solution but at least it makes the trick

Protractor Test: options to force refresh when URL hash changes

In running some protractor tests, I need to refresh the browser in between tests. I'm noticing that doesn't work when only the URL hash changes (see here).
I tried calls to .refresh() or .navigate.refresh() to force a browser refresh. That didn't seem to do anything. Possibly because of the issue referenced above since only the hash changed?
I tried .restart(), that did open a fresh browser, but it seemed to restart tests and do way more than I'm looking for.
The closest option I've had luck with was doing a .get() to a completely different URL, and then another .get() with the hash changed URL (code below). That did what I wanted in the browser, but for some reason the second .get() hangs.
What can I do to get a URL hash change to force a refresh?
beforeEach(async () => {
//First GET (to a different hash)
await browser.driver.get(browser.baseUrl);
});
it('should load ID from URL', async () => {
//Load the signup URL
let fakeId = '12345abcd';
console.log("step 1: " + new Date().toISOString());
await browser.driver.get(browser.baseUrl + `#/signup?id=${fakeId}`);
//BROWSER NAVIGATES PROPERLY, BUT NEVER GET THE CONSOLE OUTPUT BELOW
console.log("step 2: " + new Date().toISOString());
var loginButton = element(by.id('btnLoginHeader'));
var untilLoginVisible = ExpectedConditions.visibilityOf(loginButton);
await browser.wait(untilLoginVisible, 20000);
//NEVER GET THE CONSOLE OUTPUT BELOW
console.log("step 3: " + new Date().toISOString());
//<snip>
expect(true);
});
Whenever you change the hash property of ‘location’ object the browser never refreshes the page.
I think that a this scenario it is ok to force the full page reload via adding some ‘query-string’ to the path that is be fore the hash, that will load the page for sure.
Something like that:
await browser.driver.get(browser.baseUrl + `?_fake=1#/signup?id=${fakeId}`);

Chrome API's onBeforeRedirect not working properly?

I am developing a Google Chrome extension, and I need to detect redirects so I can perform a certain action (the action is irrelevant, this question simply pertains to the redirect). Thus far I have this code in my background.js:
chrome.webRequest.onBeforeRedirect.addListener(function (url, tabId) {
console.log("This is a redirect");
chrome.tabs.sendMessage(tabId, {"message": url}, function(response){});
}, {urls: ["<all_urls>"]});
However, neither the console.log or sendMessage method is getting called. I tried going to wikipedia.com, google.net, and several other sites that I know redirect the user. Why isn't the extension picking this up?
(And yes, I have put "webRequest" in my permissions under my manifest.json file.)
Thanks in advance, please let me know if you need any other code.
EDIT: Thanks to #ze it's working now, but now it's working a little too well. In other words, it's starting to fire now multiple times per page, and when I begin to type the url of a redirect site into the chrome search bar, it also gives me the redirect message. How do I only get it to fire once, when I actually push enter to navigate to the redirect site (and not while i'm still typing)? Here's the new code:
chrome.webRequest.onBeforeRedirect.addListener(function (details) {
if(details.frameId === 0){
alert("This is a redirect.");
// chrome.tabs.sendMessage(details.tabId, {"message": details.url}, function(response){});
}
}, {urls: ["<all_urls>"]});
From what I can see your callback for onBeforeRedirect has two arguments while here says that the callback should have one argument: details. Then you access url and tabId using details.url (or details.redirectUrl if you need the new url) and details.tabId.
Also, frames inside a tab may redirect as well so the event might fire more than once. If the code has to run only when the main frame redirects you should add a check like:
if (details.frameId == 0){
//Your code here
}
Also, I am not sure why you have {urls: ["<all_urls>"]} in the arguments of the addListener function. The only argument is the callback.
So, I would write something like:
chrome.webRequest.onBeforeRedirect.addListener(function (details) {
if(details.frameId == 0){
console.log("This is a redirect");
chrome.tabs.sendMessage(details.tabId, {"message": details.url}, function(response){});
}
});
Let me know if this worked out.

Categories