Skip to content

Commit 45ce680

Browse files
Mobile: fix /playlists/:id deep link routing (#14092)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ### What’s going on On mobile, the deep-link handler was treating opaque playlist ID URLs like `/playlists/:id` as **profile** links. It navigated to the `Profile` screen with `id` set to the playlist id, which leaves the profile page stuck showing its skeleton. ### Fix - Route `/playlists/:id` to the `Collection` screen with `{ id }`. - Extracted the deeplink path parsing into a small utility so it can be unit-tested. ### Tests - Added a focused Jest test covering `/users/:id` → `Profile` and `/playlists/:id` → `Collection`. - Run with: `npx jest -c packages/mobile/jest.deeplink.config.js` <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-37346ac6-0218-419a-8300-7005d3f2530b"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-37346ac6-0218-419a-8300-7005d3f2530b"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Dylan Jeffers <dylanjeffers@users.noreply.github.com>
1 parent a36dc76 commit 45ce680

8 files changed

Lines changed: 441 additions & 320 deletions

File tree

packages/mobile/babel.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = (api) => {
1010
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
1111
root: ['.'],
1212
alias: {
13+
app: './src',
1314
'@audius/common/adapters': '../common/src/adapters',
1415
'@audius/common/messages': '../common/src/messages',
1516
'@audius/common/hooks': '../common/src/hooks',
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = {
2+
testMatch: ['<rootDir>/src/components/navigation-container/**/*.test.ts'],
3+
testEnvironment: 'node',
4+
setupFilesAfterEnv: [],
5+
transformIgnorePatterns: ['/node_modules/'],
6+
moduleNameMapper: {
7+
'^react-native$': '<rootDir>/__mocks__/react-native.js',
8+
'^react-native/(.*)$': '<rootDir>/__mocks__/react-native.js',
9+
'^~/(.*)$': '<rootDir>/src/$1',
10+
'^app/(.*)$': '<rootDir>/src/$1',
11+
'^@audius/sdk$': '<rootDir>/../sdk/src',
12+
'^@audius/sdk/(.*)$': '<rootDir>/../sdk/src/$1'
13+
}
14+
}
15+

packages/mobile/jest.setup.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
require('@testing-library/jest-native/extend-expect')

packages/mobile/package.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -217,8 +217,13 @@
217217
},
218218
"jest": {
219219
"preset": "react-native",
220-
"setupFilesAfterEnv": [
221-
"@testing-library/jest-native/extend-expect"
220+
"setupFilesAfterEnv": ["<rootDir>/jest.setup.js"],
221+
"moduleNameMapper": {
222+
"^react-native$": "<rootDir>/node_modules/react-native",
223+
"^@testing-library/react-native$": "<rootDir>/node_modules/@testing-library/react-native"
224+
},
225+
"transformIgnorePatterns": [
226+
"node_modules/(?!((jest-)?react-native|@react-native(-community)?|react-native-reanimated)/)"
222227
],
223228
"moduleFileExtensions": [
224229
"ts",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { getNavigationStateFromDeeplinkPath } from 'app/utils/deeplink/getNavigationStateFromDeeplinkPath'
2+
3+
const stubGetStateFromPath = (path: string) =>
4+
path.startsWith('/track/')
5+
? { routes: [{ name: 'Track' }] }
6+
: path.startsWith('/profile')
7+
? { routes: [{ name: 'UserProfile' }] }
8+
: path.includes('/collection/')
9+
? { routes: [{ name: 'Collection' }] }
10+
: { routes: [{ name: path }] }
11+
12+
const getLeafRouteName = (state: any): string | undefined => {
13+
let current: any = state
14+
while (current?.routes?.length) {
15+
current = current.routes[current.index ?? 0]
16+
if (current?.state) current = current.state
17+
}
18+
return current?.name
19+
}
20+
21+
describe('getNavigationStateFromDeeplinkPath', () => {
22+
test('routes /users/:id to Profile', () => {
23+
const state = getNavigationStateFromDeeplinkPath({
24+
path: '/users/Nz9yBb4',
25+
options: undefined,
26+
hasAccount: true,
27+
accountHandle: 'someone',
28+
routeName: '/trending',
29+
getStateFromPath: stubGetStateFromPath as any
30+
})
31+
32+
expect(getLeafRouteName(state)).toBe('Profile')
33+
})
34+
35+
test('routes /playlists/:id to Collection', () => {
36+
const state = getNavigationStateFromDeeplinkPath({
37+
path: '/playlists/Nz9yBb4',
38+
options: undefined,
39+
hasAccount: true,
40+
accountHandle: 'someone',
41+
routeName: '/trending',
42+
getStateFromPath: stubGetStateFromPath as any
43+
})
44+
45+
expect(getLeafRouteName(state)).toBe('Collection')
46+
})
47+
48+
test('does not rewrite current user playlist permalink to /profile', () => {
49+
const state = getNavigationStateFromDeeplinkPath({
50+
path: '/Audius/playlist/140',
51+
options: undefined,
52+
hasAccount: true,
53+
accountHandle: 'Audius',
54+
routeName: '/trending',
55+
getStateFromPath: stubGetStateFromPath as any
56+
})
57+
58+
expect(getLeafRouteName(state)).toBe('Collection')
59+
})
60+
})

0 commit comments

Comments
 (0)