Skip to content

Commit

Permalink
Feature/better 404 page handling (#189)
Browse files Browse the repository at this point in the history
* chore: spa-config -> ilc-config

* feat: support of the "'ilc:404'" event

* doc: related doc update

* doc: Updated E2E tests launch instruction

* chore: updated default SSR timeout for news app, solves issue with 1st loading error

* chore: check of the errors during npm install
  • Loading branch information
StyleT committed Aug 10, 2020
1 parent 7a2af28 commit b945f79
Show file tree
Hide file tree
Showing 26 changed files with 595 additions and 167 deletions.
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, newsPage: newsPage) => {
I.amOnPage(newsPage.url.nonExistingRoute);
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', 10, 'body > div#body');
});

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

0 comments on commit b945f79

Please sign in to comment.