@lightningjs/blits 0.6.10 → 0.6.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ # v0.6.12
4
+
5
+ _9 feb 2024_
6
+
7
+ - Added type hinting for `this.$clearInterval` and `this.$clearTimeout`
8
+ - Fixed issue with `.gitignore` in app create flow
9
+ - Added support for dynamic route parts in the router (i.e `/tv/:series/episodes/:episode`)
10
+
11
+ # v0.6.11
12
+
13
+ _7 feb 2024_
14
+
15
+ - Enabled the use of `type` as a component prop or state variable
16
+ - Removed setting focus to AppComponent on back key press in RouterView
17
+ - Fixed broken app create flow caused by missing dev dependencies in npx command (temporary fix)
18
+
3
19
  # v0.6.10
4
20
 
5
21
  _2 feb 2024_
package/bin/helpers.js CHANGED
@@ -182,7 +182,7 @@ export const gitInit = (cwd, fixturesBase) => {
182
182
  .then(({ stdout }) => (msg = stdout))
183
183
  .then(() => {
184
184
  return fs.copyFileSync(
185
- path.join(fixturesBase, 'common/git/.gitignore'),
185
+ path.join(fixturesBase, 'common/git/gitignore'),
186
186
  path.join(cwd, '.gitignore')
187
187
  )
188
188
  })
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ node_modules
3
+ build
4
+ dist
5
+ env
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "0.6.10",
3
+ "version": "0.6.12",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -11,7 +11,7 @@
11
11
  },
12
12
  "scripts": {
13
13
  "test": "c8 npm run test:run",
14
- "test:run": "tape '{,!(node_modules)/**/}*.test.js' | tap-diff",
14
+ "test:run": "node -r 'global-jsdom/register' ./node_modules/.bin/tape '{,!(node_modules)/**/}*.test.js' | tap-diff",
15
15
  "lint": "eslint '**/*.js'",
16
16
  "lint:fix": "eslint '**/*.js' --fix",
17
17
  "prepublishOnly": "node scripts/prepublishOnly.js",
@@ -36,22 +36,23 @@
36
36
  "eslint": "^8.8.0",
37
37
  "eslint-config-prettier": "^8.3.0",
38
38
  "eslint-plugin-prettier": "^4.0.0",
39
- "esm": "^3.2.25",
40
- "execa": "^8.0.1",
39
+ "global-jsdom": "24.0.0",
41
40
  "husky": "^7.0.4",
42
- "kolorist": "^1.8.0",
41
+ "jsdom": "24.0.0",
43
42
  "lint-staged": "^12.3.3",
44
- "ora": "^7.0.1",
45
43
  "prettier": "^2.5.1",
46
- "prompts": "^2.4.2",
47
- "replace-in-file": "^6.1.0",
48
44
  "tap-diff": "^0.1.1",
49
- "tape": "^5.5.0",
50
- "validate-npm-package-name": "^5.0.0"
45
+ "tape": "^5.5.0"
51
46
  },
52
47
  "dependencies": {
53
48
  "@lightningjs/renderer": "^0.7.1",
54
- "@lightningjs/vite-plugin-import-chunk-url": "^0.3.0"
49
+ "@lightningjs/vite-plugin-import-chunk-url": "^0.3.0",
50
+ "execa": "^8.0.1",
51
+ "kolorist": "^1.8.0",
52
+ "ora": "^7.0.1",
53
+ "prompts": "^2.4.2",
54
+ "replace-in-file": "^6.1.0",
55
+ "validate-npm-package-name": "^5.0.0"
55
56
  },
56
57
  "repository": {
57
58
  "type": "git",
@@ -142,12 +142,22 @@ declare namespace Component {
142
142
  * Set a timeout that is automatically cleaned upon component destroy
143
143
  */
144
144
  $setTimeout: (callback: (args: any) => void, ms?: number | undefined) => ReturnType<typeof setTimeout>
145
+
146
+ /**
147
+ * Clear a timeout
148
+ */
149
+ $clearTimeout: (id: ReturnType<typeof setTimeout>) => void
145
150
 
146
151
  /**
147
152
  * Set an interval that is automatically cleaned upon component destroy
148
153
  */
149
154
  $setInterval: (callback: (args: any) => void, ms?: number | undefined) => ReturnType<typeof setInterval>
150
-
155
+
156
+ /**
157
+ * Clear a interval
158
+ */
159
+ $clearInterval: (id: ReturnType<typeof setInterval>) => void
160
+
151
161
  /**
152
162
  * Log to the console with prettier output and configurable debug levels in Settings
153
163
  */
package/src/component.js CHANGED
@@ -122,12 +122,6 @@ const Component = (name = required('name'), config = required('config')) => {
122
122
  this[symbols.wrapper] = parentEl
123
123
 
124
124
  Object.defineProperties(this, {
125
- type: {
126
- value: name,
127
- writable: false,
128
- enumerable: true,
129
- configurable: false,
130
- },
131
125
  componentId: {
132
126
  value: createHumanReadableId(name),
133
127
  writable: false,
@@ -63,8 +63,8 @@ export default () =>
63
63
  },
64
64
  },
65
65
  input: {
66
- back(e) {
67
- if (!Router.back()) this.parent.focus(e)
66
+ back() {
67
+ Router.back()
68
68
  },
69
69
  },
70
70
  })
@@ -44,8 +44,8 @@ export default () =>
44
44
  },
45
45
  },
46
46
  input: {
47
- back(e) {
48
- if (!Router.back()) this.parent.focus(e)
47
+ back() {
48
+ Router.back()
49
49
  },
50
50
  },
51
51
  })
@@ -64,14 +64,11 @@ const generateElementCode = function (
64
64
  const children = templateObject['children']
65
65
  delete templateObject['children']
66
66
 
67
- Object.keys(templateObject).forEach((key) => {
68
- if (key === 'type') {
69
- if (templateObject[key] === 'Slot') {
70
- renderCode.push(`elementConfig${counter}[Symbol.for('isSlot')] = true`)
71
- }
72
- return
73
- }
67
+ if (templateObject[Symbol.for('componentType')] === 'Slot') {
68
+ renderCode.push(`elementConfig${counter}[Symbol.for('isSlot')] = true`)
69
+ }
74
70
 
71
+ Object.keys(templateObject).forEach((key) => {
75
72
  if (key === 'slot') {
76
73
  renderCode.push(`
77
74
  elementConfig${counter}['parent'] = component[Symbol.for('slots')].filter(slot => slot.ref === '${templateObject.slot}').shift() || component[Symbol.for('slots')][0] || parent
@@ -119,8 +116,10 @@ const generateComponentCode = function (
119
116
 
120
117
  renderCode.push(`
121
118
  const cmp${counter} =
122
- (context.components && context.components['${templateObject.type}']) ||
123
- component[Symbol.for('components')]['${templateObject.type}']
119
+ (context.components && context.components['${
120
+ templateObject[Symbol.for('componentType')]
121
+ }']) ||
122
+ component[Symbol.for('components')]['${templateObject[Symbol.for('componentType')]}']
124
123
  `)
125
124
 
126
125
  if ('key' in templateObject) {
@@ -149,7 +148,6 @@ const generateComponentCode = function (
149
148
 
150
149
  renderCode.push(`const props${counter} = {}`)
151
150
  Object.keys(templateObject).forEach((key) => {
152
- if (key === 'type') return
153
151
  if (isReactiveKey(key)) {
154
152
  this.effectsCode.push(`
155
153
  ${elm}[Symbol.for('props')]['${key.substring(1)}'] = ${interpolate(
@@ -171,7 +169,13 @@ const generateComponentCode = function (
171
169
 
172
170
  renderCode.push(`
173
171
  if(!${elm}) {
174
- ${elm} = (context.components && context.components['${templateObject.type}'] || component[Symbol.for('components')]['${templateObject.type}'] || (() => { console.error('component ${templateObject.type} not found')})).call(null, {props: props${counter}}, ${parent}, component)
172
+ ${elm} = (context.components && context.components['${
173
+ templateObject[Symbol.for('componentType')]
174
+ }'] || component[Symbol.for('components')]['${
175
+ templateObject[Symbol.for('componentType')]
176
+ }'] || (() => { console.error('component ${
177
+ templateObject[Symbol.for('componentType')]
178
+ } not found')})).call(null, {props: props${counter}}, ${parent}, component)
175
179
  if (${elm}[Symbol.for('slots')][0]) {
176
180
  parent = ${elm}[Symbol.for('slots')][0]
177
181
  component = ${elm}
@@ -245,7 +249,10 @@ const generateForLoopCode = function (templateObject, parent) {
245
249
  `)
246
250
  }
247
251
 
248
- if (templateObject.type === 'Element' || templateObject.type === 'Slot') {
252
+ if (
253
+ templateObject[Symbol.for('componentType')] === 'Element' ||
254
+ templateObject[Symbol.for('componentType')] === 'Slot'
255
+ ) {
249
256
  generateElementCode.call(ctx, templateObject, parent, {
250
257
  key: 'scope.key',
251
258
  component: 'scope.',
@@ -283,7 +290,10 @@ const generateCode = function (templateObject, parent = false, options = {}) {
283
290
  if (Object.keys(childTemplateObject).indexOf(':for') > -1) {
284
291
  generateForLoopCode.call(this, childTemplateObject, parent)
285
292
  } else {
286
- if (childTemplateObject.type === 'Element' || childTemplateObject.type === 'Slot') {
293
+ if (
294
+ childTemplateObject[Symbol.for('componentType')] === 'Element' ||
295
+ childTemplateObject[Symbol.for('componentType')] === 'Slot'
296
+ ) {
287
297
  generateElementCode.call(this, childTemplateObject, parent, options)
288
298
  } else {
289
299
  generateComponentCode.call(this, childTemplateObject, parent, options)
@@ -36,4 +36,6 @@ export default {
36
36
  props: Symbol.for('props'),
37
37
  // Symbol 'slots' utilized within generated code
38
38
  slots: Symbol.for('slots'),
39
+ // Symbol 'componentType' utilized within generated code
40
+ componentType: Symbol.for('componentType'),
39
41
  }
@@ -77,7 +77,7 @@ export default (template = '', componentName, parentComponent, filePath = null)
77
77
  const match = moveCursorOnMatch(emptyTagStartRegex)
78
78
  if (match) {
79
79
  tags.push({
80
- type: null,
80
+ [Symbol.for('componentType')]: null,
81
81
  [symbols.type]: 'opening',
82
82
  [symbols.level]: currentLevel,
83
83
  [symbols.cursorTagStart]: prevCursor,
@@ -94,7 +94,7 @@ export default (template = '', componentName, parentComponent, filePath = null)
94
94
  if (match) {
95
95
  currentLevel--
96
96
  tags.push({
97
- type: null,
97
+ [Symbol.for('componentType')]: null,
98
98
  [symbols.type]: 'closing',
99
99
  [symbols.level]: currentLevel,
100
100
  })
@@ -108,7 +108,7 @@ export default (template = '', componentName, parentComponent, filePath = null)
108
108
  const match = moveCursorOnMatch(tagStartRegex)
109
109
  if (match) {
110
110
  currentTag = {
111
- type: match[1],
111
+ [Symbol.for('componentType')]: match[1],
112
112
  [symbols.level]: currentLevel,
113
113
  [symbols.cursorTagStart]: prevCursor,
114
114
  }
@@ -221,7 +221,7 @@ export default (template = '', componentName, parentComponent, filePath = null)
221
221
  [symbols.level]: element[symbols.level],
222
222
  [symbols.type]: element[symbols.type],
223
223
  [symbols.cursorTagStart]: element[symbols.cursorTagStart],
224
- type: element.type,
224
+ [Symbol.for('componentType')]: element[Symbol.for('componentType')],
225
225
  parent: currentParent, // helps getting the previous parent when closing tag is encountered
226
226
  })
227
227
  } else if (element[symbols.type] === 'closing') {
@@ -230,7 +230,9 @@ export default (template = '', componentName, parentComponent, filePath = null)
230
230
  let isTagMismatch = false
231
231
  if (!isStackEmpty) {
232
232
  isLevelMismatch = stack[stack.length - 1][symbols.level] !== element[symbols.level]
233
- isTagMismatch = stack[stack.length - 1].type !== element.type
233
+ isTagMismatch =
234
+ stack[stack.length - 1][Symbol.for('componentType')] !==
235
+ element[Symbol.for('componentType')]
234
236
  }
235
237
 
236
238
  if (isStackEmpty || isLevelMismatch || isTagMismatch) {
@@ -329,7 +331,7 @@ export default (template = '', componentName, parentComponent, filePath = null)
329
331
  let currentParent = parentComponent
330
332
 
331
333
  while (currentParent) {
332
- hierarchy = `${currentParent.type}/${hierarchy}`
334
+ hierarchy = `${currentParent[Symbol.for('componentType')]}/${hierarchy}`
333
335
  currentParent = currentParent.parent
334
336
  }
335
337
  return hierarchy
@@ -36,17 +36,68 @@ export const getHash = () => {
36
36
  return (document.location.hash || '/').replace(/^#/, '')
37
37
  }
38
38
 
39
+ const normalizePath = (path) => {
40
+ return (
41
+ path
42
+ // remove leading and trailing slashes
43
+ .replace(/^\/+|\/+$/g, '')
44
+ .toLowerCase()
45
+ )
46
+ }
47
+
39
48
  export const matchHash = (path, routes = []) => {
40
- const route = routes
41
- .filter((r) => {
42
- return r.path === path
43
- })
44
- .pop()
45
- if (route) {
46
- route.options = { ...route.options, ...overrideOptions }
47
- currentRoute = route
49
+ // remove trailing slashes
50
+ const originalPath = path
51
+ path = normalizePath(path)
52
+ let matchingRoute = false
53
+ let i = 0
54
+ while (!matchingRoute && i < routes.length) {
55
+ const route = routes[i]
56
+ route.path = normalizePath(route.path)
57
+ if (route.path === path) {
58
+ matchingRoute = route
59
+ } else if (route.path.indexOf(':') > -1) {
60
+ // match dynamic route parts
61
+ const dynamicRouteParts = [...route.path.matchAll(/:([^\s/]+)/gi)]
62
+
63
+ // construct a regex for the route with dynamic parts
64
+ let dynamicRoutePartsRegex = route.path
65
+ dynamicRouteParts.reverse().forEach((part) => {
66
+ dynamicRoutePartsRegex =
67
+ dynamicRoutePartsRegex.substring(0, part.index) +
68
+ '([^\\s/]+)' +
69
+ dynamicRoutePartsRegex.substring(part.index + part[0].length)
70
+ })
71
+
72
+ // test if the constructed regex matches the path
73
+ const match = originalPath.match(new RegExp(`${dynamicRoutePartsRegex}`, 'i'))
74
+
75
+ if (match) {
76
+ // map the route params to a params object
77
+ route.params = dynamicRouteParts.reverse().reduce((acc, part, index) => {
78
+ acc[part[1]] = match[index + 1]
79
+ return acc
80
+ }, {})
81
+ matchingRoute = route
82
+ }
83
+ } else if (route.path.endsWith('*')) {
84
+ const regex = new RegExp(route.path.replace(/\/?\*/, '/?([^\\s]*)'), 'i')
85
+ const match = path.match(regex)
86
+
87
+ if (match) {
88
+ if (match[1]) route.params = { path: match[1] }
89
+ matchingRoute = route
90
+ }
91
+ }
92
+ i++
48
93
  }
49
- return route
94
+
95
+ if (matchingRoute) {
96
+ matchingRoute.options = { ...matchingRoute.options, ...overrideOptions }
97
+ currentRoute = matchingRoute
98
+ }
99
+
100
+ return matchingRoute
50
101
  }
51
102
 
52
103
  export const navigate = async function () {
@@ -81,16 +132,18 @@ export const navigate = async function () {
81
132
  holder.populate({})
82
133
  holder.set('w', '100%')
83
134
  holder.set('h', '100%')
84
- view = await route.component(this[symbols.props], holder, this)
135
+ // merge props with potential route params to be injected into the component instance
136
+ const props = { ...this[symbols.props], ...route.params }
137
+ view = await route.component({ props }, holder, this)
85
138
  if (view[Symbol.toStringTag] === 'Module') {
86
139
  if (view.default && typeof view.default === 'function') {
87
- view = view.default(this[symbols.props], holder, this)
140
+ view = view.default({ props }, holder, this)
88
141
  } else {
89
142
  Log.error("Dynamic import doesn't have a default export or default is not a function")
90
143
  }
91
144
  }
92
145
  if (typeof view === 'function') {
93
- view = view(this[symbols.props], holder, this)
146
+ view = view({ props }, holder, this)
94
147
  }
95
148
  } else {
96
149
  holder = view[symbols.wrapper]
@@ -207,7 +260,13 @@ export const back = () => {
207
260
  if (route) {
208
261
  // set indicator that we are navigating back (to prevent adding page to history stack)
209
262
  navigatingBack = true
210
- to(route.path)
263
+ let targetRoutePath = route.path
264
+ if (targetRoutePath.indexOf(':') > -1) {
265
+ Object.keys(route.params).forEach((item) => {
266
+ targetRoutePath = targetRoutePath.replace(`:${item}`, route.params[item])
267
+ })
268
+ }
269
+ to(targetRoutePath)
211
270
  return true
212
271
  } else {
213
272
  return false
@@ -0,0 +1,238 @@
1
+ /*
2
+ * Copyright 2024 Comcast Cable Communications Management, LLC
3
+ * Licensed under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License.
5
+ * You may obtain a copy of the License at
6
+ *
7
+ * http://www.apache.org/licenses/LICENSE-2.0
8
+ *
9
+ * Unless required by applicable law or agreed to in writing, software
10
+ * distributed under the License is distributed on an "AS IS" BASIS,
11
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ * See the License for the specific language governing permissions and
13
+ * limitations under the License.
14
+ *
15
+ * SPDX-License-Identifier: Apache-2.0
16
+ */
17
+
18
+ import test from 'tape'
19
+ import { matchHash } from './router.js'
20
+
21
+ const routes = [
22
+ {
23
+ path: '/',
24
+ },
25
+ {
26
+ path: '/page1',
27
+ },
28
+ {
29
+ path: '/page1/subpage1',
30
+ },
31
+ {
32
+ path: '/tv/:show/seasons/:season',
33
+ },
34
+ {
35
+ path: '/movies/:name',
36
+ },
37
+ {
38
+ path: '/examples/*',
39
+ },
40
+ {
41
+ path: '/route/with/trailing/slash/',
42
+ },
43
+ {
44
+ path: '*',
45
+ },
46
+ ]
47
+
48
+ test('Type of matchHash', (assert) => {
49
+ const expected = 'function'
50
+ const actual = typeof matchHash
51
+
52
+ assert.equal(actual, expected, 'matchhash should be a function')
53
+ assert.end()
54
+ })
55
+
56
+ test('Match paths with static routes that have an exact match', (assert) => {
57
+ assert.equal(matchHash('/', routes), routes[0], 'Should return the correct route object')
58
+ assert.equal(matchHash('/page1', routes), routes[1], 'Should return the correct route object')
59
+ assert.equal(
60
+ matchHash('/page1/subpage1', routes),
61
+ routes[2],
62
+ 'Should return the correct route object'
63
+ )
64
+ assert.end()
65
+ })
66
+
67
+ test('Match paths with dynamic route parts', (assert) => {
68
+ assert.equal(
69
+ matchHash('/tv/simpsons/seasons/first', routes),
70
+ routes[3],
71
+ 'Should return the correct route object'
72
+ )
73
+
74
+ assert.equal(
75
+ matchHash('/movies/avengers-endgame', routes),
76
+ routes[4],
77
+ 'Should return the correct route object'
78
+ )
79
+ assert.end()
80
+ })
81
+
82
+ test('Match paths with a wildcard asterix', (assert) => {
83
+ assert.equal(
84
+ matchHash('/examples/example1', routes),
85
+ routes[5],
86
+ 'Should return the correct route object'
87
+ )
88
+
89
+ assert.equal(
90
+ matchHash('/examples/example1/subexample/and-another-page', routes),
91
+ routes[5],
92
+ 'Should return the correct route object'
93
+ )
94
+
95
+ assert.equal(matchHash('/404', routes), routes[7], 'Should return the correct route object')
96
+
97
+ assert.equal(
98
+ matchHash('/page1/subpage1/i-dont-exist', routes),
99
+ routes[7],
100
+ 'Should return the correct route object'
101
+ )
102
+
103
+ assert.end()
104
+ })
105
+
106
+ test('Add params to route for dynamic route matches', (assert) => {
107
+ const match1 = matchHash('/tv/simpsons/seasons/first', routes)
108
+
109
+ assert.deepEqual(
110
+ match1.params,
111
+ {
112
+ show: 'simpsons',
113
+ season: 'first',
114
+ },
115
+ 'Should return the correct params object'
116
+ )
117
+
118
+ const match2 = matchHash('/movies/avengers-endgame', routes)
119
+ assert.deepEqual(
120
+ match2.params,
121
+ {
122
+ name: 'avengers-endgame',
123
+ },
124
+ 'Should return the correct route object'
125
+ )
126
+
127
+ assert.end()
128
+ })
129
+
130
+ test('Add remaining path as param for wild card routes', (assert) => {
131
+ const match1 = matchHash('/examples/example1', routes)
132
+ assert.deepEqual(match1.params, { path: 'example1' }, 'Should return the correct route object')
133
+
134
+ const match2 = matchHash('/examples/example1/subexample/and-another-page', routes)
135
+ assert.deepEqual(
136
+ match2.params,
137
+ { path: 'example1/subexample/and-another-page' },
138
+ 'Should return the correct params object'
139
+ )
140
+
141
+ const match3 = matchHash('/404', routes)
142
+ assert.deepEqual(match3.params, { path: '404' }, 'Should return the correct params object')
143
+
144
+ const match4 = matchHash('/page1/subpage1/i-dont-exist', routes)
145
+ assert.deepEqual(
146
+ match4.params,
147
+ { path: 'page1/subpage1/i-dont-exist' },
148
+ 'Should return the correct params object'
149
+ )
150
+
151
+ assert.end()
152
+ })
153
+
154
+ test('Work with trailing slashes', (assert) => {
155
+ assert.equal(matchHash('/page1/', routes), routes[1], 'Should return the correct route object')
156
+ assert.equal(
157
+ matchHash('/page1//////', routes),
158
+ routes[1],
159
+ 'Should return the correct route object'
160
+ )
161
+ assert.equal(
162
+ matchHash('/page1/subpage1/', routes),
163
+ routes[2],
164
+ 'Should return the correct route object'
165
+ )
166
+
167
+ assert.equal(
168
+ matchHash('/route/with/trailing/slash', routes),
169
+ routes[6],
170
+ 'Should return the correct route object'
171
+ )
172
+
173
+ assert.end()
174
+ })
175
+
176
+ test('Work with and without leading slashes', (assert) => {
177
+ assert.equal(matchHash('page1/', routes), routes[1], 'Should return the correct route object')
178
+
179
+ assert.equal(matchHash('////page1', routes), routes[1], 'Should return the correct route object')
180
+ assert.equal(
181
+ matchHash('page1/subpage1/', routes),
182
+ routes[2],
183
+ 'Should return the correct route object'
184
+ )
185
+
186
+ assert.equal(
187
+ matchHash('route/with/trailing/slash', routes),
188
+ routes[6],
189
+ 'Should return the correct route object'
190
+ )
191
+
192
+ assert.end()
193
+ })
194
+
195
+ test('Match routes case insensitive', (assert) => {
196
+ assert.equal(matchHash('/', routes), routes[0], 'Should return the correct route object')
197
+ assert.equal(matchHash('/Page1', routes), routes[1], 'Should return the correct route object')
198
+ assert.equal(
199
+ matchHash('/pagE1/SUBpage1', routes),
200
+ routes[2],
201
+ 'Should return the correct route object'
202
+ )
203
+ assert.equal(
204
+ matchHash('/Tv/SimpsonS/Seasons/First', routes),
205
+ routes[3],
206
+ 'Should return the correct route object'
207
+ )
208
+
209
+ assert.equal(
210
+ matchHash('/Movies/Avengers-Endgame', routes),
211
+ routes[4],
212
+ 'Should return the correct route object'
213
+ )
214
+ assert.end()
215
+ })
216
+
217
+ test('Match routes case insensitive, but pass props with original casing', (assert) => {
218
+ const match1 = matchHash('/Tv/SimpsonS/Seasons/First', routes)
219
+ assert.deepEqual(
220
+ match1.params,
221
+ {
222
+ show: 'SimpsonS',
223
+ season: 'First',
224
+ },
225
+ 'Should return the correct params object'
226
+ )
227
+
228
+ const match2 = matchHash('/Movies/Avengers-Endgame', routes)
229
+ assert.deepEqual(
230
+ match2.params,
231
+ {
232
+ name: 'Avengers-Endgame',
233
+ },
234
+ 'Should return the correct params object'
235
+ )
236
+
237
+ assert.end()
238
+ })