fix: throw if the same pattern resolves to multiple screens

This commit is contained in:
Satyajit Sahoo
2020-11-08 23:38:19 +01:00
parent e08c91ff0a
commit 48b2e77730
3 changed files with 80 additions and 0 deletions

View File

@@ -2673,6 +2673,47 @@ it('uses nearest parent wildcard match for unmatched paths', () => {
);
});
it('throws if two screens map to the same pattern', () => {
const path = '/bar/42/baz/test';
expect(() =>
getStateFromPath(path, {
screens: {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
Baz: 'baz',
},
},
Bax: '/bar/:id/baz',
},
},
},
})
).toThrow(
"Found conflicting screens with the same pattern. The pattern 'bar/:id/baz' resolves to both 'Foo > Bax' and 'Foo > Bar > Baz'. Patterns must be unique and cannot resolve to more than one screen."
);
expect(() =>
getStateFromPath(path, {
screens: {
Foo: {
screens: {
Bar: {
path: '/bar/:id/',
screens: {
Baz: '',
},
},
},
},
},
})
).not.toThrow();
});
it('throws if wildcard is specified with legacy config', () => {
const path = '/bar/42/baz/test';
const config = {

View File

@@ -239,6 +239,10 @@ export default function getPathFromState(
// Object.fromEntries is not available in older iOS versions
const fromEntries = <K extends string, V>(entries: (readonly [K, V])[]) =>
entries.reduce((acc, [k, v]) => {
if (acc.hasOwnProperty(k)) {
throw new Error(`A value for key '${k}' already exists in the object.`);
}
acc[k] = v;
return acc;
}, {} as Record<K, V>);

View File

@@ -119,6 +119,12 @@ export default function getStateFromPath(
// - the most exhaustive ones are always at the beginning
// - patterns with wildcard are always at the end
// If 2 patterns are same, move the one with less route names up
// This is an error state, so it's only useful for consistent error messages
if (a.pattern === b.pattern) {
return b.routeNames.join('>').localeCompare(a.routeNames.join('>'));
}
// If one of the patterns starts with the other, it's more exhaustive
// So move it up
if (a.pattern.startsWith(b.pattern)) {
@@ -155,6 +161,35 @@ export default function getStateFromPath(
return bWildcardIndex - aWildcardIndex;
});
// Check for duplicate patterns in the config
configs.reduce<Record<string, RouteConfig>>((acc, config) => {
if (acc[config.pattern]) {
const a = acc[config.pattern].routeNames;
const b = config.routeNames;
// It's not a problem if the path string omitted from a inner most screen
// For example, it's ok if a path resolves to `A > B > C` or `A > B`
const intersects =
a.length > b.length
? b.every((it, i) => a[i] === it)
: a.every((it, i) => b[i] === it);
if (!intersects) {
throw new Error(
`Found conflicting screens with the same pattern. The pattern '${
config.pattern
}' resolves to both '${a.join(' > ')}' and '${b.join(
' > '
)}'. Patterns must be unique and cannot resolve to more than one screen.`
);
}
}
return Object.assign(acc, {
[config.pattern]: config,
});
}, {});
if (remaining === '/') {
// We need to add special handling of empty path so navigation to empty path also works
// When handling empty path, we should only look at the root level config