diff --git a/core/examples/luigi-sample-angular/e2e/tests/luigi-client-features.spec.js b/core/examples/luigi-sample-angular/e2e/tests/luigi-client-features.spec.js index 2a0fc95e42..0fdbb9df3e 100644 --- a/core/examples/luigi-sample-angular/e2e/tests/luigi-client-features.spec.js +++ b/core/examples/luigi-sample-angular/e2e/tests/luigi-client-features.spec.js @@ -140,6 +140,39 @@ describe('Luigi client features', () => { }); }); + describe('linkManager wrong paths navigation', () => { + let $iframeBody; + beforeEach(() => { + cy.get('iframe').then($iframe => { + $iframeBody = $iframe.contents().find('body'); + cy.goToFeaturesPage($iframeBody); + }); + }); + it('navigate to a partly wrong link', () => { + cy.wrap($iframeBody) + .contains('Partly wrong link') + .click(); + cy.location().should(loc => { + expect(loc.hash).to.eq('#/projects/pr2/miscellaneous2'); + }); + cy.get('.fd-alert').contains( + 'Could not map the exact target node for the requested route projects/pr2/miscellaneous2/maskopatol' + ); + }); + + it('navigate to a totally wrong link', () => { + cy.wrap($iframeBody) + .contains('Totally wrong link') + .click(); + cy.location().should(loc => { + expect(loc.hash).to.eq('#/overview'); + }); + cy.get('.fd-alert').contains( + 'Could not find the requested route maskopatol/has/a/child' + ); + }); + }); + describe('uxManager', () => { it('backdrop', () => { cy.wait(500); diff --git a/core/examples/luigi-sample-angular/src/app/project/project.component.html b/core/examples/luigi-sample-angular/src/app/project/project.component.html index 78929db696..619bb6ce76 100644 --- a/core/examples/luigi-sample-angular/src/app/project/project.component.html +++ b/core/examples/luigi-sample-angular/src/app/project/project.component.html @@ -1,76 +1,99 @@
-
-

Project {{ projectId }}

-
-
-

LuigiClient uxManager methods:

- -

- - -

-
+
+

Project {{ projectId }}

+
+
+

LuigiClient uxManager methods:

+ +

+ + +

+
-
- -
+
+ +
- +
+
+

LuigiClient - wrong paths in linkManager.navigate():

+ +
+
+ + \ No newline at end of file diff --git a/core/package.json b/core/package.json index fd2ec32ef0..e72da3b443 100644 --- a/core/package.json +++ b/core/package.json @@ -36,7 +36,7 @@ "scripts": { "bundle": "webpack --display-error-details", "bundle-develop": "webpack -d --watch", - "test": "./node_modules/mocha/bin/mocha --require babel-register --require babel-polyfill --require jsdom-global/register", + "test": "./node_modules/mocha/bin/mocha --require babel-register --require babel-polyfill --require jsdom-global/register --recursive test", "bundlesize": "bundlesize", "prepush": "npm test && npm run bundle && npm run bundlesize" }, diff --git a/core/src/App.html b/core/src/App.html index a51f59f209..dd6813c87e 100644 --- a/core/src/App.html +++ b/core/src/App.html @@ -1,4 +1,10 @@
+ {#if alert && alert.message} + + {/if}
@@ -122,12 +128,14 @@ }; const handleNavigation = async (component, data, config) => { - const path = buildPath(component, data.params); - const matchedPath = await Routing.matchPath(path); - if (matchedPath !== null) { - addPreserveView(component, data, config); - Routing.navigateTo(matchedPath); + let path = buildPath(component, data.params); + + if (path[0] !== '/') { + path = '/' + path; //add leading slash if necessary } + + addPreserveView(component, data, config); + Routing.navigateTo(path); //navigate to the raw path. Any errors/alerts are handled later }; const sendContextToClient = (component, config, goBackContext = {}) => { @@ -296,6 +304,7 @@ diff --git a/core/src/services/routing.js b/core/src/services/routing.js index 101701935a..50234b8ccb 100644 --- a/core/src/services/routing.js +++ b/core/src/services/routing.js @@ -3,6 +3,7 @@ import { LuigiConfig } from './config'; import { getPathWithoutHash, getUrlWithoutHash, + containsAllSegments, isIE, getConfigValueFromObject } from '../utilities/helpers'; @@ -308,10 +309,31 @@ export const handleRouteChange = async (path, component, node, config) => { if (routeExists) { const defaultChildNode = getDefaultChildNode(pathData); navigateTo(`${pathUrl ? `/${pathUrl}` : ''}/${defaultChildNode}`); - } // TODO else display 404 page + } else { + const alert = { + message: 'Could not find the requested route', + link: pathUrl + }; + + component.set({ alert }); + navigateTo('/'); + //error 404 + } return; } + if (!containsAllSegments(pathUrl, pathData.navigationPath)) { + const matchedPath = await matchPath(pathUrl); + + const alert = { + message: 'Could not map the exact target node for the requested route', + link: pathUrl + }; + + component.set({ alert }); + navigateTo(matchedPath); + } + const previousCompData = component.get(); component.set({ hideNav, @@ -392,7 +414,7 @@ export const matchPath = async path => { @param windowElem object defaults to window @param documentElem object defaults to document */ -export const navigateTo = (route, windowElem = window) => { +export const navigateTo = async (route, windowElem = window) => { if (LuigiConfig.getConfigValue('routing.useHashRouting')) { windowElem.location.hash = route; return; diff --git a/core/src/utilities/helpers.js b/core/src/utilities/helpers.js index eeb4f44b33..28fb769d33 100644 --- a/core/src/utilities/helpers.js +++ b/core/src/utilities/helpers.js @@ -86,6 +86,29 @@ export const prependOrigin = path => { return window.location.origin; }; +export const containsAllSegments = (sourceUrl, targetPathSegments) => { + if (!sourceUrl || !targetPathSegments || !targetPathSegments.length) { + console.error( + 'Ooops, seems like the developers have misconfigured something' + ); + return false; + } + + const pathSegmentsUrl = targetPathSegments + .slice(1) + .map(x => x.pathSegment) + .join('/'); + const mandatorySegmentsUrl = removeTrailingSlash(sourceUrl.split('?')[0]); + return pathSegmentsUrl === mandatorySegmentsUrl; +}; + +/** + * Prepend current url to redirect_uri, if it is a relative path + * @param {str} string from which any number of trailing slashes should be removed + * @returns string string without any trailing slash + */ +export const removeTrailingSlash = str => str.replace(/\/+$/, ''); + /* * Gets value of the given property on the given object. */ diff --git a/core/test/config.spec.js b/core/test/services/config.spec.js similarity index 95% rename from core/test/config.spec.js rename to core/test/services/config.spec.js index d33a807327..1e8e46f587 100644 --- a/core/test/config.spec.js +++ b/core/test/services/config.spec.js @@ -1,6 +1,6 @@ const chai = require('chai'); const assert = chai.assert; -import { LuigiConfig } from '../src/services/config'; +import { LuigiConfig } from '../../src/services/config'; describe('Config', () => { describe('getConfigBooleanValue()', () => { diff --git a/core/test/navigation.spec.js b/core/test/services/navigation.spec.js similarity index 98% rename from core/test/navigation.spec.js rename to core/test/services/navigation.spec.js index cde45d9dde..96d92c99fd 100644 --- a/core/test/navigation.spec.js +++ b/core/test/services/navigation.spec.js @@ -1,9 +1,9 @@ -const navigation = require('../src/navigation/services/navigation'); +const navigation = require('../../src/navigation/services/navigation'); const chai = require('chai'); const expect = chai.expect; const assert = chai.assert; const sinon = require('sinon'); -import { LuigiConfig } from '../src/services/config'; +import { LuigiConfig } from '../../src/services/config'; const sampleNavPromise = new Promise(function(resolve) { const lazyLoadedChildrenNodesProviderFn = () => { diff --git a/core/test/routing.spec.js b/core/test/services/routing.spec.js similarity index 98% rename from core/test/routing.spec.js rename to core/test/services/routing.spec.js index 7c3c9927e4..ebee0aef0a 100644 --- a/core/test/routing.spec.js +++ b/core/test/services/routing.spec.js @@ -4,10 +4,10 @@ const expect = chai.expect; const assert = chai.assert; const sinon = require('sinon'); const MockBrowser = require('mock-browser').mocks.MockBrowser; -const routing = require('../src/services/routing'); -import { deepMerge } from '../src/utilities/helpers.js'; +const routing = require('../../src/services/routing'); +import { deepMerge } from '../../src/utilities/helpers.js'; import { afterEach } from 'mocha'; -import { LuigiConfig } from '../src/services/config'; +import { LuigiConfig } from '../../src/services/config'; describe('Routing', () => { let component; @@ -742,7 +742,7 @@ describe('Routing', () => { }); describe('defaultChildNodes', () => { - const routing = rewire('../src/services/routing'); + const routing = rewire('../../src/services/routing'); const getDefaultChildNode = routing.__get__('getDefaultChildNode'); const getPathData = function() { return { diff --git a/core/test/utilities/helpers.spec.js b/core/test/utilities/helpers.spec.js new file mode 100644 index 0000000000..2eaff926e9 --- /dev/null +++ b/core/test/utilities/helpers.spec.js @@ -0,0 +1,106 @@ +const chai = require('chai'); +const assert = chai.assert; +import { containsAllSegments } from '../../src/utilities/helpers'; + +describe('Helpers()', () => { + describe('#containsAllSegments()', () => { + it('should return true when proper data provided', async () => { + const sourceUrl = 'mas/ko/pa/tol/'; + const targetPathSegments = [ + { + //doesn't matter, it's omitted anyway + }, + { + pathSegment: 'mas' + }, + { + pathSegment: 'ko' + }, + { + pathSegment: 'pa' + }, + { + pathSegment: 'tol' + } + ]; + assert.equal(containsAllSegments(sourceUrl, targetPathSegments), true); + }); + + it('should return false when wrong data provided', async () => { + const differentSourceUrl = 'mas/ko/pa/tol'; + const similarSourceUrl = 'luigi/is/os/awesome'; + const targetPathSegments = [ + { + //doesn't matter, it's omitted anyway + }, + { + pathSegment: 'luigi' + }, + { + pathSegment: 'is' + }, + { + pathSegment: 'so' + }, + { + pathSegment: 'awesome' + } + ]; + assert.equal( + containsAllSegments(differentSourceUrl, targetPathSegments), + false + ); + assert.equal( + containsAllSegments(similarSourceUrl, targetPathSegments), + false + ); + }); + + it("should return false when pathSegments numbers don't match", async () => { + const tooShortSourceUrl = 'one/two'; + const tooLongSourceUrl = 'three/four/five/six'; + const targetPathSegments = [ + { + //doesn't matter, it's omitted anyway + }, + { + pathSegment: 'one' + }, + { + pathSegment: 'two' + }, + { + pathSegment: 'three' + } + ]; + assert.equal( + containsAllSegments(tooShortSourceUrl, targetPathSegments), + false + ); + assert.equal( + containsAllSegments(tooLongSourceUrl, targetPathSegments), + false + ); + }); + + it('should ignore GET parameters', async () => { + const sourceUrl = 'one/two/three?masko=patol&four=five'; + + const targetPathSegments = [ + { + //doesn't matter, it's omitted anyway + }, + { + pathSegment: 'one' + }, + { + pathSegment: 'two' + }, + { + pathSegment: 'three' + } + ]; + assert.equal(containsAllSegments(sourceUrl, targetPathSegments), true); + }); + }); +});