Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/better 404 page handling #189

Merged
merged 17 commits into from
Aug 10, 2020
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ At the same time in parallel terminal you need to run `npm run start:no-apps`
To make sure that all ILC components play well together we use E2E tests. We use our Demo applications as test micro frontends
so it also gives us ability to make sure that we don't break backward compatibility.

In order to run tests - change your current directory to `./e2e` and launch one of the following commands:

* Default mode: `npm start`
* Verbose mode: `npm run start:verbose`
* Verbose mode with Browser UI visible: `npm run start:verbose:ui`
In order to run tests:

* Build ILC & Registry by running `npm run build`
* Change your current directory to `./e2e`
* Launch one of the following commands:
* Default mode: `npm start`
* Verbose mode: `npm run start:verbose`
* Verbose mode with Browser UI visible: `npm run start:verbose:ui`

16 changes: 7 additions & 9 deletions docs/global_errors_handling.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
# Global error handling with ILC

**Status codes:** ✅ - implemented; 🎯 - to be implemented soon

Introduction of Micro Frontends architecture brings new challenges to the board that need to be solved. One of those challenges is handling of the 5XX & 4XX error pages. As your web page now is composed from several fragments you can't use the same approach you used to have with monolithic frontend. This is the moment when ILC comes to the stage.

Types of the apps
Types of the apps
-----------------

- **Primary** - app which is "responsible" for the current route, usually this app renders main consumable page content. At a particular route you may have only a single primary app. During SSR this app will supply HTTP response code & headers in the response to the client.
Expand All @@ -13,7 +11,7 @@ Introduction of Micro Frontends architecture brings new challenges to the board

- **Regular** - app which provides supplementary functionality on the page. Page can be effectively consumed by users w/o such apps rendered. Example: footer, ads, promo banners

"5XX" errors (unexpected errors)
"5XX" errors (unexpected errors)
--------------------------------

Handling of the unexpected errors varies between SSR & CSR (as well it depends on the type of the app) due to the natural differences between server & client. So keep an eye on it.
Expand All @@ -34,18 +32,18 @@ It's also worth saying that there is no such thing as 5XX error at the client si
- _CSR:_ any error caught by ILC errorHander or errors during loading/mounting - will be logged w/o any further actions


✅/🎯 "404" error (Not found)
"404" error (Not found)
-----------------------

This is a very common error in web applications & usually it means that we want to show some message to the user that requested resource was not found on the server.

With the introduction of the micro frontends & global ILC router things become a little bit trickier. It means that we may catch this error at 2 different routing layers:

- **ILC Router** – if there is no route configured in Registry for requested URL - it will trigger an appearance of the special 404 route ([Namecheap example](https://www.namecheap.com/status/404.aspx)). This logic will work seamlessly between SSR & CSR.
- **ILC Router** – if there is no route configured in Registry for requested URL - it will trigger an appearance of the special 404 route ([Namecheap example](https://www.namecheap.com/status/404.aspx)). This logic will work seamlessly between SSR & CSR.

Ex: `/nosuchpath` url was requested. Or try <http://demo.microfrontends.online/nosuchpath>

- 🎯 **App Router** – (_only for primary apps_) there also may be cases when we have a route configured in Registry, however the app which is responsible for the page - fails to find the requested resource by it's ID. Imagine that you're trying to open a page of the non-existing product. Here there are 2 ways for the app to handle this case:
- **App Router** – (_only for primary apps_) there also may be cases when we have a route configured in Registry, however the app which is responsible for the page - fails to find the requested resource by it's ID. Imagine that you're trying to open a page of the non-existing product. Here there are 2 ways for the app to handle this case:
- _Fallback to global 404 page_ - recommended approach, in this case app's content will be abandoned and users will see content of the special 404 route. To do this at CSR/SSR do the following:
- _SSR:_ respond with 404 HTTP code
- _CSR:_ trigger `ilc:404` event on window with the following parameters:
Expand All @@ -57,7 +55,7 @@ With the introduction of the micro frontends & global ILC router things become a



🎯 "401" & "403" errors (Unauthorized / Forbidden)
"401" & "403" errors (Unauthorized / Forbidden)
-----------------------

TBD, currently ILC has no special logic in place
Сurrently ILC has no special logic in place. May be reconsidered in the future.
81 changes: 79 additions & 2 deletions e2e/spec/404.spec.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,83 @@
Feature('404');
Feature('404 error handling');

Scenario('should show 404 error page', (I) => {
//region 404 page for non-existing ILC route
Scenario('Renders (SSR) global 404 page for non-existing ILC route', (I) => {
I.amOnPage('/nonexistent-path');
I.waitForText('404 not found', 10, 'body > div#body');
});
Scenario('Renders (CSR) global 404 page for non-existing ILC route', (I, peoplePage) => {
const notFoundPageLink = '#navbar a[href="/nosuchpath"]';

I.amOnPage(peoplePage.peopleUrl);
I.waitForElement(notFoundPageLink, 30);
I.click(notFoundPageLink);
I.waitForText('404 not found', 10, 'body > div#body');
});
//endregion 404 page for non-existing ILC route

//region 404 page for non-existing News app route
Scenario('Renders (SSR) global 404 page for non-existing News app route', (I) => {
I.amOnPage('/news/nonExisting');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see you added URLs here is e2e/spec/pages/news.ts
So you can use newsPage.url.nonExistingRoute here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

I.waitForText('404 not found', 10, 'body > div#body');
});

Scenario('Renders (CSR) global 404 page for non-existing News app route', (I, newsPage: newsPage) => {
I.amOnPage(newsPage.url.main);
I.waitInUrl(newsPage.url.main, 10);
I.waitForElement(newsPage.linkWithUrl(newsPage.url.nonExistingRoute), 10);
I.click(newsPage.linkWithUrl(newsPage.url.nonExistingRoute));
I.waitForText('404 not found', 10, 'body > div#body');

//After 404 page ILC continues normal operation
I.click(newsPage.linkWithUrl(newsPage.url.main));
I.waitForElement(newsPage.newsSources, 10);
I.see('Pick a news source', newsPage.bannerHeadline);
});

Scenario('Renders (SSR) overridden 404 page for non-existing News app route', (I, newsPage: newsPage) => {
I.amOnPage(newsPage.url.nonExistingRouteWithOverride);
I.waitForText('404 not found component', 10, 'body > div#body');
});

Scenario('Renders (CSR) overridden 404 page for non-existing News app route', (I, newsPage: newsPage) => {
I.amOnPage(newsPage.url.main);
I.waitInUrl(newsPage.url.main, 10);
I.waitForElement(newsPage.linkWithUrl(newsPage.url.nonExistingRouteWithOverride), 10);
I.click(newsPage.linkWithUrl(newsPage.url.nonExistingRouteWithOverride));
I.waitForText('404 not found component', 10, 'body > div#body');
});
//endregion 404 page for non-existing News app route

//region 404 page for non-existing News resource
Scenario('Renders (SSR) global 404 page for non-existing News resource', (I, newsPage: newsPage) => {
I.amOnPage(newsPage.url.nonExistingResource);
I.waitForText('404 not found', 10, 'body > div#body');
});

Scenario('Renders (CSR) global 404 page for non-existing News resource', (I, newsPage: newsPage) => {
I.amOnPage(newsPage.url.main);
I.waitInUrl(newsPage.url.main, 10);
I.waitForElement(newsPage.linkWithUrl(newsPage.url.nonExistingResource), 10);
I.click(newsPage.linkWithUrl(newsPage.url.nonExistingResource));
I.waitForText('404 not found', 10, 'body > div#body');

//After 404 page ILC continues normal operation
I.wait(5); //Hack to fix issue with the Vue Router
I.click(newsPage.linkWithUrl(newsPage.url.main));
I.waitForElement(newsPage.newsSources, 10);
I.see('Pick a news source', newsPage.bannerHeadline);
});

Scenario('Renders (SSR) overridden 404 page for non-existing News resource', (I, newsPage: newsPage) => {
I.amOnPage(newsPage.url.nonExistingResourceWithOverride);
I.waitForText('404 not found component', 1000, 'body > div#body');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we exactly need to wait here for 1000 seconds?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

});

Scenario('Renders (CSR) overridden 404 page for non-existing News resource', (I, newsPage: newsPage) => {
I.amOnPage(newsPage.url.main);
I.waitInUrl(newsPage.url.main, 10);
I.waitForElement(newsPage.linkWithUrl(newsPage.url.nonExistingResourceWithOverride), 10);
I.click(newsPage.linkWithUrl(newsPage.url.nonExistingResourceWithOverride));
I.waitForText('404 not found component', 10, 'body > div#body');
});
//endregion 404 page for non-existing News resource
14 changes: 7 additions & 7 deletions e2e/spec/navbar.spec.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ Feature('navbar ilc demo application');

Scenario('should open every page and show a content only of an opened page', async (I, peoplePage, newsPage, planetsPage) => {
I.amOnPage('/');
I.waitForElement(newsPage.goToNews, 10);
I.click(newsPage.goToNews);
I.waitInUrl(newsPage.newsUrl, 10);
I.seeAttributesOnElements(newsPage.goToNews, {
I.waitForElement(newsPage.linkWithUrl(newsPage.url.main), 10);
I.click(newsPage.linkWithUrl(newsPage.url.main));
I.waitInUrl(newsPage.url.main, 10);
I.seeAttributesOnElements(newsPage.linkWithUrl(newsPage.url.main), {
'aria-current': 'page',
});
I.waitForElement(newsPage.newsSources, 10);
Expand Down Expand Up @@ -59,9 +59,9 @@ Scenario('should open every page and show a content only of an opened page', asy
I.waitForClickable(peoplePage.fetchMorePeople, 10);
I.seeNumberOfVisibleElements(peoplePage.personsList, 10);

I.click(newsPage.goToNews);
I.waitInUrl(newsPage.newsUrl, 10);
I.seeAttributesOnElements(newsPage.goToNews, {
I.click(newsPage.linkWithUrl(newsPage.url.main));
I.waitInUrl(newsPage.url.main, 10);
I.seeAttributesOnElements(newsPage.linkWithUrl(newsPage.url.main), {
'aria-current': 'page',
});
I.waitForElement(newsPage.newsSources, 10);
Expand Down
18 changes: 9 additions & 9 deletions e2e/spec/news.spec.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ Feature('news ilc demo application');

Scenario('should open a news page and show news sources', async (I, newsPage: newsPage) => {
I.amOnPage('/');
I.waitForElement(newsPage.goToNews, 10);
I.click(newsPage.goToNews);
I.waitInUrl(newsPage.newsUrl, 10);
I.seeAttributesOnElements(newsPage.goToNews, {
I.waitForElement(newsPage.linkWithUrl(newsPage.url.main), 10);
I.click(newsPage.linkWithUrl(newsPage.url.main));
I.waitInUrl(newsPage.url.main, 10);
I.seeAttributesOnElements(newsPage.linkWithUrl(newsPage.url.main), {
'aria-current': 'page',
});
I.waitForElement(newsPage.newsSources, 10);
I.see('Pick a news source', newsPage.bannerHeadline);
});

Scenario('should open an article page from a direct link', async (I, newsPage: newsPage) => {
I.amOnPage(newsPage.newsUrl);
I.waitInUrl(newsPage.newsUrl, 10);
I.amOnPage(newsPage.url.main);
I.waitInUrl(newsPage.url.main, 10);
I.waitForElement(newsPage.newsSources, 10);
I.scrollPageToBottom();

Expand All @@ -38,10 +38,10 @@ Scenario('should open an article page from a direct link', async (I, newsPage: n
});

Scenario('should open 500 error page when an error happens', async (I, newsPage: newsPage) => {
I.amOnPage(newsPage.newsUrl);
I.waitInUrl(newsPage.newsUrl, 10);
I.amOnPage(newsPage.url.main);
I.waitInUrl(newsPage.url.main, 10);
I.waitForElement(newsPage.generateError, 10);
I.click(newsPage.generateError);
I.waitForElement(newsPage.errorId);
I.seeInCurrentUrl(newsPage.newsUrl);
I.seeInCurrentUrl(newsPage.url.main);
});
12 changes: 9 additions & 3 deletions e2e/spec/pages/news.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
const { I } = inject();

export const newsUrl = '/news/';
export const url = {
main: '/news/',
nonExistingRoute: '/news/nonExisting',
nonExistingRouteWithOverride: '/news/nonExisting?overrideErrorPage=1',
nonExistingResource: '/news/article/abc-news-au34',
nonExistingResourceWithOverride: '/news/article/abc-news-au34?overrideErrorPage=1',
};

export const linkWithUrl = (url:string) => `a[href="${url}"]`;

export const goToNews = `body > div#navbar a[href="${newsUrl}"]`;
export const newsView = 'body > div#body > div.single-spa-container.news-app > div.view';
export const goToNewsSources = `${newsView} > div.container > p.home > a[href="${newsUrl}"]`;
export const newsSources = `${newsView} > div.sources > div.container > ol > li.source`;
export const bannerHeadline = `${newsView} > div.banner > h1`;
export const generateError = `${newsView} > div.banner > a`;
Expand Down
8 changes: 5 additions & 3 deletions ilc/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import Router from './client/ClientRouter';
import setupErrorHandlers from './client/errorHandler/setupErrorHandlers';
import {fragmentErrorHandlerFactory, crashIlc} from './client/errorHandler/fragmentErrorHandlerFactory';
import isActiveFactory from './client/isActiveFactory';
import initSpaConfig from './client/initSpaConfig';
import initIlcConfig from './client/initIlcConfig';
import initIlcState from './client/initIlcState';
import setupPerformanceMonitoring from './client/performance';
import selectSlotsToRegister from './client/selectSlotsToRegister';
import {getSlotElement} from './client/utils';
Expand All @@ -16,8 +17,9 @@ if (System === undefined) {
throw new Error('ILC: can\'t find SystemJS on a page, crashing everything');
}

const registryConf = initSpaConfig();
const router = new Router(registryConf, singleSpa.navigateToUrl);
const registryConf = initIlcConfig();
const state = initIlcState();
const router = new Router(registryConf, state, singleSpa);
const asyncBootUp = new AsyncBootUp();

selectSlotsToRegister([...registryConf.routes, registryConf.specialRoutes['404']]).forEach((slots) => {
Expand Down
Loading