From 2f7e28fedd3028bf6f103e0fe15897bd5b903346 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 12 Jul 2022 16:08:29 +0200 Subject: [PATCH] core-plugin-api: split collectors tests into 3 suites Co-authored-by: blam Signed-off-by: Patrik Oldsberg --- .../src/routing/collectors.beta.test.tsx | 373 ++++++++++++++++++ ...rs.test.tsx => collectors.compat.test.tsx} | 55 ++- .../src/routing/collectors.stable.test.tsx | 373 ++++++++++++++++++ 3 files changed, 787 insertions(+), 14 deletions(-) create mode 100644 packages/core-app-api/src/routing/collectors.beta.test.tsx rename packages/core-app-api/src/routing/{collectors.test.tsx => collectors.compat.test.tsx} (95%) create mode 100644 packages/core-app-api/src/routing/collectors.stable.test.tsx diff --git a/packages/core-app-api/src/routing/collectors.beta.test.tsx b/packages/core-app-api/src/routing/collectors.beta.test.tsx new file mode 100644 index 0000000000..1c5bc8c639 --- /dev/null +++ b/packages/core-app-api/src/routing/collectors.beta.test.tsx @@ -0,0 +1,373 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { PropsWithChildren } from 'react'; +import { routingV1Collector } from './collectors'; + +import { + traverseElementTree, + childDiscoverer, + routeElementDiscoverer, +} from '../extensions/traversal'; +import { + createRoutableExtension, + createRouteRef, + createPlugin, + RouteRef, + attachComponentData, + BackstagePlugin, +} from '@backstage/core-plugin-api'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; + +const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( + <>{children} +); + +const plugin = createPlugin({ id: 'my-plugin' }); + +const ref1 = createRouteRef({ id: 'ref1' }); +const ref2 = createRouteRef({ id: 'ref2' }); +const ref3 = createRouteRef({ id: 'ref3' }); +const ref4 = createRouteRef({ id: 'ref4' }); +const ref5 = createRouteRef({ id: 'ref5' }); +const refOrder = [ref1, ref2, ref3, ref4, ref5]; + +const Extension1 = plugin.provide( + createRoutableExtension({ + name: 'Extension1', + component: () => Promise.resolve(MockComponent), + mountPoint: ref1, + }), +); +const Extension2 = plugin.provide( + createRoutableExtension({ + name: 'Extension2', + component: () => Promise.resolve(MockComponent), + mountPoint: ref2, + }), +); +const Extension3 = plugin.provide( + createRoutableExtension({ + name: 'Extension3', + component: () => Promise.resolve(MockComponent), + mountPoint: ref3, + }), +); +const Extension4 = plugin.provide( + createRoutableExtension({ + name: 'Extension4', + component: () => Promise.resolve(MockComponent), + mountPoint: ref4, + }), +); +const Extension5 = plugin.provide( + createRoutableExtension({ + name: 'Extension5', + component: () => Promise.resolve(MockComponent), + mountPoint: ref5, + }), +); + +const AggregationComponent = ({ + children, +}: PropsWithChildren<{ + path: string; +}>) => <>{children}; + +attachComponentData(AggregationComponent, 'core.gatherMountPoints', true); + +function sortedEntries(map: Map): [RouteRef, T][] { + return Array.from(map).sort( + ([a], [b]) => refOrder.indexOf(a) - refOrder.indexOf(b), + ); +} + +function routeObj( + path: string, + refs: RouteRef[], + children: any[] = [], + type: 'mounted' | 'gathered' = 'mounted', + backstagePlugin?: BackstagePlugin, +) { + return { + path: path, + caseSensitive: false, + element: type, + routeRefs: new Set(refs), + children: [ + { + path: '/*', + caseSensitive: false, + element: 'match-all', + routeRefs: new Set(), + }, + ...children, + ], + plugin: backstagePlugin, + }; +} + +describe('discovery', () => { + it('should collect routes', () => { + const list = [ +
, +
, +
+ +
, + ]; + + const root = ( + + + +
+ +
+
+ Some text here shouldn't be a problem +
+ {null} +
+ +
+ + {false} + {list} + {true} + {0} +
+ +
+ } /> +
+ + + ); + + const { routing } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar/:id'], + [ref3, '/baz'], + [ref4, '/divsoup'], + [ref5, '/blop'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, ref2], + [ref4, undefined], + [ref5, ref1], + ]); + expect(routing.objects).toEqual([ + routeObj( + '/foo', + [ref1], + [ + routeObj('/bar/:id', [ref2], [routeObj('/baz', [ref3])]), + routeObj('/blop', [ref5]), + ], + ), + routeObj('/divsoup', [ref4], undefined, undefined, plugin), + ]); + }); + + it('should handle all react router Route patterns', () => { + const root = ( + + + + + + + + } + /> + }> + } /> + + + + + ); + + const { routing } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar/:id'], + [ref3, '/baz'], + [ref4, '/divsoup'], + [ref5, '/blop'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, undefined], + [ref4, ref3], + [ref5, ref3], + ]); + }); + + it('should use the route aggregator key to bind child routes to the same path', () => { + const root = ( + + + + +
+ +
+ HELLO +
+ + + + + + + +
+
+ ); + + const { routing } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/foo'], + [ref3, '/bar'], + [ref4, '/baz'], + [ref5, '/baz'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, undefined], + [ref3, undefined], + [ref4, ref3], + [ref5, ref3], + ]); + expect(routing.objects).toEqual([ + routeObj('/foo', [ref1, ref2], [], 'gathered'), + routeObj( + '/bar', + [ref3], + [routeObj('/baz', [ref4, ref5], [], 'gathered')], + ), + ]); + }); + + it('should use the route aggregator but stop when encountering explicit path', () => { + const root = ( + + + + + + + + + + + + + + + ); + + const { routing } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar'], + [ref3, '/baz'], + [ref4, '/blop'], + [ref5, '/bar'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, ref1], + [ref4, ref3], + [ref5, ref1], + ]); + expect(routing.objects).toEqual([ + routeObj( + '/foo', + [ref1], + [ + routeObj( + '/bar', + [ref2, ref5], + [routeObj('/baz', [ref3], [routeObj('/blop', [ref4])])], + 'gathered', + ), + ], + ), + ]); + }); + + it('should stop gathering mount points after encountering explicit path', () => { + const root = ( + + + + + + + + + + + + ); + + expect(() => { + traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + }).toThrow('Mounted routable extension must have a path'); + }); +}); diff --git a/packages/core-app-api/src/routing/collectors.test.tsx b/packages/core-app-api/src/routing/collectors.compat.test.tsx similarity index 95% rename from packages/core-app-api/src/routing/collectors.test.tsx rename to packages/core-app-api/src/routing/collectors.compat.test.tsx index 08f5a3bc35..276cc380da 100644 --- a/packages/core-app-api/src/routing/collectors.test.tsx +++ b/packages/core-app-api/src/routing/collectors.compat.test.tsx @@ -16,17 +16,25 @@ import React from 'react'; import type { PropsWithChildren } from 'react'; -import type { RouteRef, BackstagePlugin } from '@backstage/core-plugin-api'; +import { + RouteRef, + BackstagePlugin, + createPlugin, + createRouteRef, + createRoutableExtension, + attachComponentData, +} from '@backstage/core-plugin-api'; +import { Collector, Discoverer } from '../extensions/traversal'; -describe.each(['beta', 'stable'])('react-router %s', v => { +describe.each(['beta', 'stable'])('react-router %s', rrVersion => { beforeAll(() => { jest.doMock('react-router', () => - v === 'beta' + rrVersion === 'beta' ? jest.requireActual('react-router') : jest.requireActual('react-router-stable'), ); jest.doMock('react-router-dom', () => - v === 'beta' + rrVersion === 'beta' ? jest.requireActual('react-router-dom') : jest.requireActual('react-router-dom-stable'), ); @@ -48,11 +56,6 @@ describe.each(['beta', 'stable'])('react-router %s', v => { jest.clearAllMocks(); }); - it('does something', () => { - const { Route } = requireDeps(); - console.log('DEBUG: Route =', Route); - }); - const MockComponent = ({ children, }: PropsWithChildren<{ path?: string }>) => <>{children}; @@ -141,8 +144,34 @@ describe.each(['beta', 'stable'])('react-router %s', v => { }; } - describe('routingV1Collector', () => { + describe(`routing with ${rrVersion}`, () => { + let traversalOptions: { + discoverers: Discoverer[]; + collectors: { routing: Collector }; + }; + beforeEach(() => { + const { + routingV1Collector, + routingV2Collector, + childDiscoverer, + routeElementDiscoverer, + } = requireDeps(); + + traversalOptions = { + discoverers: + rrVersion === 'beta' + ? [childDiscoverer, routeElementDiscoverer] + : [childDiscoverer], + collectors: { + routing: + rrVersion === 'beta' ? routingV1Collector : routingV2Collector, + }, + }; + }); + it('should collect routes', () => { + const { MemoryRouter, Routes, Route, traverseElementTree } = + requireDeps(); const list = [
,
, @@ -181,11 +210,9 @@ describe.each(['beta', 'stable'])('react-router %s', v => { const { routing } = traverseElementTree({ root, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - routing: routingV1Collector, - }, + ...traversalOptions, }); + expect(sortedEntries(routing.paths)).toEqual([ [ref1, '/foo'], [ref2, '/bar/:id'], diff --git a/packages/core-app-api/src/routing/collectors.stable.test.tsx b/packages/core-app-api/src/routing/collectors.stable.test.tsx new file mode 100644 index 0000000000..1c5bc8c639 --- /dev/null +++ b/packages/core-app-api/src/routing/collectors.stable.test.tsx @@ -0,0 +1,373 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { PropsWithChildren } from 'react'; +import { routingV1Collector } from './collectors'; + +import { + traverseElementTree, + childDiscoverer, + routeElementDiscoverer, +} from '../extensions/traversal'; +import { + createRoutableExtension, + createRouteRef, + createPlugin, + RouteRef, + attachComponentData, + BackstagePlugin, +} from '@backstage/core-plugin-api'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; + +const MockComponent = ({ children }: PropsWithChildren<{ path?: string }>) => ( + <>{children} +); + +const plugin = createPlugin({ id: 'my-plugin' }); + +const ref1 = createRouteRef({ id: 'ref1' }); +const ref2 = createRouteRef({ id: 'ref2' }); +const ref3 = createRouteRef({ id: 'ref3' }); +const ref4 = createRouteRef({ id: 'ref4' }); +const ref5 = createRouteRef({ id: 'ref5' }); +const refOrder = [ref1, ref2, ref3, ref4, ref5]; + +const Extension1 = plugin.provide( + createRoutableExtension({ + name: 'Extension1', + component: () => Promise.resolve(MockComponent), + mountPoint: ref1, + }), +); +const Extension2 = plugin.provide( + createRoutableExtension({ + name: 'Extension2', + component: () => Promise.resolve(MockComponent), + mountPoint: ref2, + }), +); +const Extension3 = plugin.provide( + createRoutableExtension({ + name: 'Extension3', + component: () => Promise.resolve(MockComponent), + mountPoint: ref3, + }), +); +const Extension4 = plugin.provide( + createRoutableExtension({ + name: 'Extension4', + component: () => Promise.resolve(MockComponent), + mountPoint: ref4, + }), +); +const Extension5 = plugin.provide( + createRoutableExtension({ + name: 'Extension5', + component: () => Promise.resolve(MockComponent), + mountPoint: ref5, + }), +); + +const AggregationComponent = ({ + children, +}: PropsWithChildren<{ + path: string; +}>) => <>{children}; + +attachComponentData(AggregationComponent, 'core.gatherMountPoints', true); + +function sortedEntries(map: Map): [RouteRef, T][] { + return Array.from(map).sort( + ([a], [b]) => refOrder.indexOf(a) - refOrder.indexOf(b), + ); +} + +function routeObj( + path: string, + refs: RouteRef[], + children: any[] = [], + type: 'mounted' | 'gathered' = 'mounted', + backstagePlugin?: BackstagePlugin, +) { + return { + path: path, + caseSensitive: false, + element: type, + routeRefs: new Set(refs), + children: [ + { + path: '/*', + caseSensitive: false, + element: 'match-all', + routeRefs: new Set(), + }, + ...children, + ], + plugin: backstagePlugin, + }; +} + +describe('discovery', () => { + it('should collect routes', () => { + const list = [ +
, +
, +
+ +
, + ]; + + const root = ( + + + +
+ +
+
+ Some text here shouldn't be a problem +
+ {null} +
+ +
+ + {false} + {list} + {true} + {0} +
+ +
+ } /> +
+ + + ); + + const { routing } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar/:id'], + [ref3, '/baz'], + [ref4, '/divsoup'], + [ref5, '/blop'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, ref2], + [ref4, undefined], + [ref5, ref1], + ]); + expect(routing.objects).toEqual([ + routeObj( + '/foo', + [ref1], + [ + routeObj('/bar/:id', [ref2], [routeObj('/baz', [ref3])]), + routeObj('/blop', [ref5]), + ], + ), + routeObj('/divsoup', [ref4], undefined, undefined, plugin), + ]); + }); + + it('should handle all react router Route patterns', () => { + const root = ( + + + + + + + + } + /> + }> + } /> + + + + + ); + + const { routing } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar/:id'], + [ref3, '/baz'], + [ref4, '/divsoup'], + [ref5, '/blop'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, undefined], + [ref4, ref3], + [ref5, ref3], + ]); + }); + + it('should use the route aggregator key to bind child routes to the same path', () => { + const root = ( + + + + +
+ +
+ HELLO +
+ + + + + + + +
+
+ ); + + const { routing } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/foo'], + [ref3, '/bar'], + [ref4, '/baz'], + [ref5, '/baz'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, undefined], + [ref3, undefined], + [ref4, ref3], + [ref5, ref3], + ]); + expect(routing.objects).toEqual([ + routeObj('/foo', [ref1, ref2], [], 'gathered'), + routeObj( + '/bar', + [ref3], + [routeObj('/baz', [ref4, ref5], [], 'gathered')], + ), + ]); + }); + + it('should use the route aggregator but stop when encountering explicit path', () => { + const root = ( + + + + + + + + + + + + + + + ); + + const { routing } = traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + expect(sortedEntries(routing.paths)).toEqual([ + [ref1, '/foo'], + [ref2, '/bar'], + [ref3, '/baz'], + [ref4, '/blop'], + [ref5, '/bar'], + ]); + expect(sortedEntries(routing.parents)).toEqual([ + [ref1, undefined], + [ref2, ref1], + [ref3, ref1], + [ref4, ref3], + [ref5, ref1], + ]); + expect(routing.objects).toEqual([ + routeObj( + '/foo', + [ref1], + [ + routeObj( + '/bar', + [ref2, ref5], + [routeObj('/baz', [ref3], [routeObj('/blop', [ref4])])], + 'gathered', + ), + ], + ), + ]); + }); + + it('should stop gathering mount points after encountering explicit path', () => { + const root = ( + + + + + + + + + + + + ); + + expect(() => { + traverseElementTree({ + root, + discoverers: [childDiscoverer, routeElementDiscoverer], + collectors: { + routing: routingV1Collector, + }, + }); + }).toThrow('Mounted routable extension must have a path'); + }); +});