@lightningjs/blits 1.23.2 → 1.25.0

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.
@@ -4,6 +4,7 @@ on:
4
4
  pull_request_target:
5
5
  branches:
6
6
  - dev
7
+ - master
7
8
 
8
9
  jobs:
9
10
  run-blits-tests:
@@ -12,6 +13,9 @@ jobs:
12
13
  steps:
13
14
  - name: Checkout Code
14
15
  uses: actions/checkout@v3
16
+ with:
17
+ ref: ${{ github.event.pull_request.head.ref }}
18
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
15
19
 
16
20
  - name: Set up Node.js
17
21
  uses: actions/setup-node@v3
@@ -42,7 +46,7 @@ jobs:
42
46
  echo "summary=$summary" >> $GITHUB_OUTPUT
43
47
 
44
48
  # If the number of failed tests is greater than 1, exit with error.
45
- if [ "$failed" -gt 1 ]; then
49
+ if [ "$failed" -gt 0 ]; then
46
50
  exit 1
47
51
  fi
48
52
 
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.25.0
4
+
5
+ _28 mar 2025_
6
+
7
+ - Fixed issue when same effect type is used in `effects` attribute
8
+ - Added ability to enable / disable announcer via launch setting as well as runtime
9
+
10
+ ## v1.24.0
11
+
12
+ _26 mar 2025_
13
+
14
+ - Added additional tests for router
15
+ - Fixed issues with `:show` attribute not working properly
16
+ - Fixed issue with strings starting with a number being casted to a number
17
+ - Fixed named slots functionality
18
+
3
19
  ## v1.23.2
4
20
 
5
21
  _18 mar 2025_
@@ -46,3 +46,15 @@ Blits.Component('MyComponent', {
46
46
  }
47
47
  })
48
48
  ```
49
+
50
+ ## Enabling / disabling the announcer
51
+
52
+ By default the announcer is disabled. This mean that whenever you call `this.$announcer.speak()` there will actually not be anything spoken out via the Text To Speech engine.
53
+
54
+ In order to enable utterances being spoken out, you can set the Blits launch setting `announcer` to `true` in the index.js.
55
+
56
+ Alternatively the announcer can be enabled or disabled run time by using one of the following methods on the Announcer pluging:
57
+
58
+ - `this.$announcer.enable()` - activates the announcer
59
+ - `this.$announcer.disable()` - deactivates the announcer
60
+ - `this.$announcer.disable(true/false)` - turns the announcer or on off
package/index.d.ts CHANGED
@@ -929,7 +929,22 @@ declare module '@lightningjs/blits' {
929
929
  *
930
930
  * @important if you dont know what you're doing here, you probably shouldn't be doing it!
931
931
  */
932
- advanced?: Partial<RendererMainSettings>
932
+ advanced?: Partial<RendererMainSettings>,
933
+ /**
934
+ * Whether or not the announcer should be activated on initialization
935
+ *
936
+ * When set to `false` announcements via `this.$annoucer.speak()` will
937
+ * be ignored. When set to `true` announcement will be spoken out via the
938
+ * text to speech API
939
+ *
940
+ * Announcer can be enabled / disabled run time as well via:
941
+ * - this.$announcer.enable()
942
+ * - this.$announcer.disable()
943
+ * - this.$announcer.toggle(true/false)
944
+ *
945
+ * @default false
946
+ */
947
+ announcer?: boolean
933
948
  }
934
949
 
935
950
  interface State {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lightningjs/blits",
3
- "version": "1.23.2",
3
+ "version": "1.25.0",
4
4
  "description": "Blits: The Lightning 3 App Development Framework",
5
5
  "bin": "bin/index.js",
6
6
  "exports": {
@@ -19,7 +19,22 @@ import speechSynthesis from './speechSynthesis.js'
19
19
 
20
20
  let debounce
21
21
 
22
+ let active = false
23
+
24
+ const enable = () => {
25
+ active = true
26
+ }
27
+
28
+ const disable = () => {
29
+ active = false
30
+ }
31
+
32
+ const toggle = (v) => {
33
+ active = v ? true : false
34
+ }
35
+
22
36
  const speak = (message, politeness = 'off') => {
37
+ if (active === false) return
23
38
  clearTimeout(debounce)
24
39
  speechSynthesis.cancel()
25
40
  // assertive messages get spoken immediately
@@ -45,4 +60,7 @@ export default {
45
60
  polite,
46
61
  assertive,
47
62
  stop,
63
+ enable,
64
+ disable,
65
+ toggle,
48
66
  }
@@ -54,6 +54,8 @@ const Application = (config) => {
54
54
  }
55
55
 
56
56
  config.hooks[symbols.init] = function () {
57
+ // set the initial activation state of the announcer based on launch setting
58
+ this.$announcer.toggle(Settings.get('announcer', false))
57
59
  const keyMap = { ...defaultKeyMap, ...Settings.get('keymap', {}) }
58
60
 
59
61
  keyDownHandler = async (e) => {
@@ -736,7 +736,7 @@ export default () =>
736
736
  )
737
737
 
738
738
  elementConfig23['image'] = component.image
739
- elementConfig23['x'] = -2
739
+ elementConfig23['x'] = '-2'
740
740
  elementConfig23['w'] = 47
741
741
  elementConfig23['h'] = 25
742
742
  elementConfig23['map'] = component.sprite
@@ -759,7 +759,7 @@ export default () =>
759
759
  parent = elms[23]
760
760
  const props24 = {}
761
761
  props24['image'] = component.image
762
- props24['x'] = -2
762
+ props24['x'] = '-2'
763
763
  props24['w'] = 47
764
764
  props24['h'] = 25
765
765
  props24['map'] = component.sprite
@@ -82,6 +82,9 @@ const layoutFn = function (config) {
82
82
  let otherDimension = 0
83
83
  const gap = config.gap || 0
84
84
  for (let i = 0; i < childrenLength; i++) {
85
+ if (this.children[i] !== undefined && this.children[i].props.raw.show === false) {
86
+ continue
87
+ }
85
88
  const node = children[i]
86
89
  node[position] = offset
87
90
  node[oppositePosition] = padding.oppositeStart
@@ -303,12 +306,8 @@ const propsTransformer = {
303
306
  set show(v) {
304
307
  if (v) {
305
308
  this.props['alpha'] = this.raw['alpha'] !== undefined ? this.raw['alpha'] : 1
306
- this.props['width'] = this.raw['w'] || this.raw['width'] || 0
307
- this.props['height'] = this.raw['h'] || this.raw['height'] || 0
308
309
  } else {
309
310
  this.props['alpha'] = 0
310
- this.props['width'] = 0
311
- this.props['height'] = 0
312
311
  }
313
312
  },
314
313
  set alpha(v) {
@@ -329,24 +328,29 @@ const propsTransformer = {
329
328
  v[i].props.color = colors.normalize(v[i].props.color)
330
329
  }
331
330
  }
332
-
331
+ const effectNames = {}
333
332
  if (this.element.node === undefined) {
333
+ this.element.effectNames = []
334
334
  this.props['shader'] = renderer.createShader('DynamicShader', {
335
335
  effects: v.map((effect) => {
336
+ let name = effect.type
337
+ if (effectNames[name] !== undefined) {
338
+ name += ++effectNames[name]
339
+ } else {
340
+ effectNames[name] = 1
341
+ }
342
+ name += this.element.counter
343
+ this.element.effectNames.push(name)
336
344
  // temporary add counter to work around shader caching issues
337
- return renderer.createEffect(
338
- effect.type,
339
- effect.props,
340
- effect.type + this.element.counter
341
- )
345
+ return renderer.createEffect(effect.type, effect.props, name)
342
346
  }),
343
347
  })
344
348
  } else {
345
349
  for (let i = 0; i < v.length; i++) {
350
+ const name = this.element.effectNames[i]
346
351
  // temporary add counter to work around shader caching issues
347
- const target = this.element.node.shader.props[v[i].type + this.element.counter]
352
+ const target = this.element.node.shader.props[name]
348
353
  const props = Object.keys(v[i].props)
349
-
350
354
  if (target == undefined) continue
351
355
  for (let j = 0; j < props.length; j++) {
352
356
  target[props[j]] = v[i].props[props[j]]
@@ -652,6 +656,7 @@ export default (config, component) => {
652
656
  scheduledTransitions: {},
653
657
  config,
654
658
  component,
659
+ effectNames: [],
655
660
  counter: counter++,
656
661
  })
657
662
  }
@@ -551,10 +551,6 @@ test('Element - Set `show` property as false', (assert) => {
551
551
 
552
552
  assert.equal(el.node['alpha'], 0, 'Node alpha parameter should set to 0')
553
553
  assert.equal(el.props.props['alpha'], 0, 'Props alpha parameter should set')
554
- assert.equal(el.node['width'], 0, 'Node width parameter should set to 0')
555
- assert.equal(el.props.props['width'], 0, 'props width parameter should set to 0')
556
- assert.equal(el.node['height'], 0, 'Node height parameter should set to 0')
557
- assert.equal(el.props.props['height'], 0, 'props height parameter should set to 0')
558
554
 
559
555
  assert.end()
560
556
  })
@@ -567,9 +563,7 @@ test('Element - Set `show` property as false', (assert) => {
567
563
  assert.equal(el.node['alpha'], 1, 'Node alpha parameter should be set')
568
564
  assert.equal(el.props.props['alpha'], 1, 'Props alpha parameter should be set')
569
565
  assert.equal(el.node['width'], 960, 'Node width parameter should be set')
570
- assert.equal(el.props.props['width'], 960, 'props width parameter should be set')
571
566
  assert.equal(el.node['height'], 540, 'Node height parameter should be set')
572
- assert.equal(el.props.props['height'], 540, 'props height parameter should be set')
573
567
 
574
568
  assert.end()
575
569
  })
@@ -148,7 +148,7 @@ const generateElementCode = function (
148
148
  Object.keys(templateObject).forEach((key) => {
149
149
  if (key === 'slot') {
150
150
  renderCode.push(`
151
- elementConfig${counter}['parent'] = component[Symbol.for('slots')] !== undefined && Array.isArray(component[Symbol.for('slots')]) === true && component[Symbol.for('slots')].filter(slot => slot.ref === '${templateObject.slot}').shift() || parent
151
+ elementConfig${counter}['parent'] = slotComponent[Symbol.for('slots')] !== undefined && Array.isArray(slotComponent[Symbol.for('slots')]) === true && slotComponent[Symbol.for('slots')].filter(slot => slot.ref === '${templateObject.slot}').shift() || parent
152
152
  `)
153
153
  }
154
154
 
@@ -691,7 +691,7 @@ const cast = (val = '', key = false, component = 'component.') => {
691
691
  }
692
692
  }
693
693
  // numeric
694
- else if (key !== 'color' && !isNaN(parseFloat(val))) {
694
+ else if (key !== 'color' && /[^0-9%\s.eE]/.test(val) === false && !isNaN(parseFloat(val))) {
695
695
  castedValue = parseFloat(val)
696
696
  if (val.endsWith('%')) {
697
697
  const map = {
@@ -296,6 +296,71 @@ test('Generate code for a template with a single element with attributes with a
296
296
  assert.end()
297
297
  })
298
298
 
299
+ test('Generate code for a template with a single element with attributes with numeric & alpha numeric value', (assert) => {
300
+ const templateObject = {
301
+ children: [
302
+ {
303
+ [Symbol.for('componentType')]: 'Element',
304
+ x: '10e2',
305
+ y: '100',
306
+ h: '45%',
307
+ w: '45.45%',
308
+ one: 'Pure String',
309
+ two: '123abc',
310
+ three: '1E',
311
+ four: 'E1',
312
+ },
313
+ ],
314
+ }
315
+
316
+ const expectedRender = `
317
+ function anonymous(parent, component, context, components, effect, getRaw, Log) {
318
+ const elms = []
319
+ let componentType
320
+ const rootComponent = component
321
+ let propData
322
+ let slotComponent
323
+ let inSlot = false
324
+ let slotChildCounter = 0
325
+ const elementConfig0 = {}
326
+
327
+ elms[0] = this.element(
328
+ { parent: parent || 'root' },
329
+ inSlot === true ? slotComponent : component
330
+ )
331
+
332
+ elementConfig0['x'] = 1000
333
+ elementConfig0['y'] = 100
334
+ elementConfig0['h'] = parent.node.height * (45 / 100)
335
+ elementConfig0['w'] = parent.node.width * (45.45 / 100)
336
+ elementConfig0['one'] = "Pure String"
337
+ elementConfig0['two'] = "123abc"
338
+ elementConfig0['three'] = 1
339
+ elementConfig0['four'] = "E1"
340
+
341
+ elms[0].populate(elementConfig0)
342
+
343
+ if (inSlot === true) {
344
+ slotChildCounter -= 1
345
+ }
346
+
347
+ return elms
348
+ }
349
+ `
350
+
351
+ const actual = generator.call(scope, templateObject)
352
+ assert.equal(
353
+ normalize(actual.render.toString()),
354
+ normalize(expectedRender),
355
+ 'Generator should return a render function with the correct code'
356
+ )
357
+ assert.ok(
358
+ Array.isArray(actual.effects) && actual.effects.length === 0,
359
+ 'Generator should return an empty effects array'
360
+ )
361
+ assert.end()
362
+ })
363
+
299
364
  test('Generate code for a template with attributes and a nested element with attributes', (assert) => {
300
365
  const templateObject = {
301
366
  children: [
@@ -1427,6 +1492,144 @@ test('Generate code for a template with custom components with arguments', (asse
1427
1492
  assert.end()
1428
1493
  })
1429
1494
 
1495
+ test('Generate code for a template with custom components with argument value as numeric & alpha numeric', (assert) => {
1496
+ const templateObject = {
1497
+ children: [
1498
+ {
1499
+ [Symbol.for('componentType')]: 'Element',
1500
+ children: [
1501
+ {
1502
+ [Symbol.for('componentType')]: 'Poster',
1503
+ x: '10e2',
1504
+ y: '100',
1505
+ h: '45%',
1506
+ w: '45.45%',
1507
+ one: 'Pure String',
1508
+ two: '123abc',
1509
+ three: '1E',
1510
+ four: 'E1',
1511
+ },
1512
+ ],
1513
+ },
1514
+ ],
1515
+ }
1516
+
1517
+ const scope = {
1518
+ components: {
1519
+ Poster: () => {},
1520
+ },
1521
+ }
1522
+
1523
+ const expectedRender = `
1524
+ function anonymous(parent, component, context, components, effect, getRaw, Log) {
1525
+ const elms = []
1526
+ let componentType
1527
+ const rootComponent = component
1528
+ let propData
1529
+ let slotComponent
1530
+ let inSlot = false
1531
+ let slotChildCounter = 0
1532
+ const elementConfig0 = {}
1533
+
1534
+ elms[0] = this.element(
1535
+ { parent: parent || 'root' },
1536
+ inSlot === true ? slotComponent : component
1537
+ )
1538
+ elms[0].populate(elementConfig0)
1539
+
1540
+ if (inSlot === true) {
1541
+ slotChildCounter -= 1
1542
+ }
1543
+
1544
+ const cmp1 = (context.components && context.components['Poster']) || components['Poster']
1545
+ parent = elms[0]
1546
+ const elementConfig1 = {}
1547
+
1548
+ elms[1] = this.element(
1549
+ { parent: parent || 'root' },
1550
+ inSlot === true ? slotComponent : component
1551
+ )
1552
+
1553
+ elementConfig1['x'] = 1000
1554
+ elementConfig1['y'] = 100
1555
+ elementConfig1['h'] = parent.node.height * (45 / 100)
1556
+ elementConfig1['w'] = parent.node.width * (45.45 / 100)
1557
+ elementConfig1['one'] = "PureString"
1558
+ elementConfig1['two'] = "123abc"
1559
+ elementConfig1['three'] = 1
1560
+ elementConfig1['four'] = "E1"
1561
+
1562
+ const skip1 = []
1563
+ if (typeof cmp1 !== 'undefined') {
1564
+ for (let key in cmp1.config.props) {
1565
+ delete elementConfig1[cmp1.config.props[key]]
1566
+ skip1.push(cmp1.config.props[key])
1567
+ }
1568
+ }
1569
+
1570
+ elms[1].populate(elementConfig1)
1571
+
1572
+ if (inSlot === true) {
1573
+ slotChildCounter -= 1
1574
+ }
1575
+
1576
+ parent = elms[1]
1577
+ const props2 = {}
1578
+
1579
+ props2['x'] = 1000
1580
+ props2['y'] = 100
1581
+ props2['h'] = parent.node.height * (45 / 100)
1582
+ props2['w'] = parent.node.width * (45.45 / 100)
1583
+ props2['one'] = "PureString"
1584
+ props2['two'] = "123abc"
1585
+ props2['three'] = 1
1586
+ props2['four'] = "E1"
1587
+
1588
+ componentType = props2['is'] || 'Poster'
1589
+ let component2
1590
+
1591
+ if (typeof componentType === 'string') {
1592
+ component2 = context.components && context.components[componentType] || components[componentType]
1593
+ if (!component2) {
1594
+ throw new Error('Component "Poster" not found')
1595
+ }
1596
+ } else if (typeof componentType === 'function' && componentType[Symbol.for('isComponent')] === true) {
1597
+ component2 = componentType
1598
+ }
1599
+
1600
+ elms[2] = component2.call(null, { props: props2 }, elms[1], component)
1601
+
1602
+ if (elms[2][Symbol.for('slots')][0]) {
1603
+ parent = elms[2][Symbol.for('slots')][0]
1604
+ slotComponent = elms[2]
1605
+ inSlot = true
1606
+ } else {
1607
+ parent = elms[2][Symbol.for('children')][0]
1608
+ }
1609
+
1610
+ if (inSlot === true && slotChildCounter === 0) {
1611
+ inSlot = false
1612
+ }
1613
+
1614
+ return elms
1615
+ }
1616
+ `
1617
+
1618
+ const actual = generator.call(scope, templateObject)
1619
+
1620
+ assert.equal(
1621
+ normalize(actual.render.toString()),
1622
+ normalize(expectedRender),
1623
+ 'Generator should return a render function with the correct code'
1624
+ )
1625
+ assert.ok(
1626
+ Array.isArray(actual.effects) && actual.effects.length === 0,
1627
+ 'Generator should return an empty effects array'
1628
+ )
1629
+
1630
+ assert.end()
1631
+ })
1632
+
1430
1633
  test('Generate code for a template with custom components with reactive props', (assert) => {
1431
1634
  const templateObject = {
1432
1635
  children: [
@@ -1933,9 +2136,9 @@ test('Generate code for a template with slot content, using a named slot', (asse
1933
2136
  elms[4] = this.element({ parent: parent || 'root' }, inSlot === true ? slotComponent : component)
1934
2137
  elementConfig4['x'] = 100
1935
2138
  elementConfig4['y'] = component.y
1936
- elementConfig4['parent'] = component[Symbol.for('slots')] !== undefined &&
1937
- Array.isArray(component[Symbol.for('slots')]) === true &&
1938
- component[Symbol.for('slots')].filter(slot => slot.ref === 'mySlot').shift() || parent
2139
+ elementConfig4['parent'] = slotComponent[Symbol.for('slots')] !== undefined &&
2140
+ Array.isArray(slotComponent[Symbol.for('slots')]) === true &&
2141
+ slotComponent[Symbol.for('slots')].filter(slot => slot.ref === 'mySlot').shift() || parent
1939
2142
  elementConfig4['slot'] = "mySlot"
1940
2143
  elms[4].populate(elementConfig4)
1941
2144
 
@@ -66,7 +66,7 @@ const isString = (v) => typeof v === 'string'
66
66
 
67
67
  export const matchHash = (path, routes = []) => {
68
68
  // remove trailing slashes
69
- const originalPath = path
69
+ const originalPath = path.replace(/^\/+|\/+$/g, '')
70
70
  path = normalizePath(path)
71
71
  let matchingRoute = false
72
72
  let i = 0
@@ -89,6 +89,8 @@ export const matchHash = (path, routes = []) => {
89
89
  dynamicRoutePartsRegex.substring(part.index + part[0].length)
90
90
  })
91
91
 
92
+ dynamicRoutePartsRegex = '^' + dynamicRoutePartsRegex
93
+
92
94
  // test if the constructed regex matches the path
93
95
  const match = originalPath.match(new RegExp(`${dynamicRoutePartsRegex}`, 'i'))
94
96
 
@@ -16,7 +16,7 @@
16
16
  */
17
17
 
18
18
  import test from 'tape'
19
- import { matchHash } from './router.js'
19
+ import { matchHash, getHash, to } from './router.js'
20
20
 
21
21
  const routes = [
22
22
  {
@@ -31,6 +31,9 @@ const routes = [
31
31
  {
32
32
  path: '/tv/:show/seasons/:season',
33
33
  },
34
+ {
35
+ path: '/movies/special',
36
+ },
34
37
  {
35
38
  path: '/movies/:name',
36
39
  },
@@ -40,6 +43,9 @@ const routes = [
40
43
  {
41
44
  path: '/route/with/trailing/slash/',
42
45
  },
46
+ {
47
+ path: '/dutchmovies/:id',
48
+ },
43
49
  {
44
50
  path: '*',
45
51
  },
@@ -54,6 +60,7 @@ test('Type of matchHash', (assert) => {
54
60
  })
55
61
 
56
62
  test('Match paths with static routes that have an exact match', (assert) => {
63
+ assert.equal(matchHash('', routes), routes[0], 'Should return the correct route object')
57
64
  assert.equal(matchHash('/', routes), routes[0], 'Should return the correct route object')
58
65
  assert.equal(matchHash('/page1', routes), routes[1], 'Should return the correct route object')
59
66
  assert.equal(
@@ -61,6 +68,11 @@ test('Match paths with static routes that have an exact match', (assert) => {
61
68
  routes[2],
62
69
  'Should return the correct route object'
63
70
  )
71
+ assert.equal(
72
+ matchHash('/movies/special', routes),
73
+ routes[4],
74
+ 'Should return the correct route object'
75
+ )
64
76
  assert.end()
65
77
  })
66
78
 
@@ -71,32 +83,69 @@ test('Match paths with dynamic route parts', (assert) => {
71
83
  'Should return the correct route object'
72
84
  )
73
85
 
86
+ assert.equal(
87
+ matchHash('/tv/simpsons/season/first', routes),
88
+ routes[9],
89
+ 'Should return the correct route object'
90
+ )
91
+
92
+ assert.equal(
93
+ matchHash('/examples/test/seasons/5', routes),
94
+ routes[6],
95
+ 'Should return the correct route object'
96
+ )
97
+
74
98
  assert.equal(
75
99
  matchHash('/movies/avengers-endgame', routes),
76
- routes[4],
100
+ routes[5],
77
101
  'Should return the correct route object'
78
102
  )
103
+
104
+ assert.equal(
105
+ matchHash('/movies/special-1', routes),
106
+ routes[5],
107
+ 'Should return the correct route object'
108
+ )
109
+
110
+ assert.equal(
111
+ matchHash('/dutchmovies/123', routes),
112
+ routes[8],
113
+ 'Should return the correct route object'
114
+ )
115
+
79
116
  assert.end()
80
117
  })
81
118
 
82
119
  test('Match paths with a wildcard asterix', (assert) => {
83
120
  assert.equal(
84
121
  matchHash('/examples/example1', routes),
85
- routes[5],
122
+ routes[6],
86
123
  'Should return the correct route object'
87
124
  )
88
125
 
89
126
  assert.equal(
90
127
  matchHash('/examples/example1/subexample/and-another-page', routes),
91
- routes[5],
128
+ routes[6],
92
129
  'Should return the correct route object'
93
130
  )
94
131
 
95
- assert.equal(matchHash('/404', routes), routes[7], 'Should return the correct route object')
132
+ assert.equal(matchHash('/404', routes), routes[9], 'Should return the correct route object')
133
+
134
+ assert.equal(
135
+ matchHash('/page1/non-existing-path', routes),
136
+ routes[9],
137
+ 'Should return the correct route object'
138
+ )
96
139
 
97
140
  assert.equal(
98
141
  matchHash('/page1/subpage1/i-dont-exist', routes),
99
- routes[7],
142
+ routes[9],
143
+ 'Should return the correct route object'
144
+ )
145
+
146
+ assert.equal(
147
+ matchHash('/completely/unexpected/route', routes),
148
+ routes[9],
100
149
  'Should return the correct route object'
101
150
  )
102
151
 
@@ -124,6 +173,15 @@ test('Add params to route for dynamic route matches', (assert) => {
124
173
  'Should return the correct route object'
125
174
  )
126
175
 
176
+ const match3 = matchHash('/dutchmovies/1', routes)
177
+ assert.deepEqual(
178
+ match3.params,
179
+ {
180
+ id: '1',
181
+ },
182
+ 'Should return the correct route object'
183
+ )
184
+
127
185
  assert.end()
128
186
  })
129
187
 
@@ -166,7 +224,13 @@ test('Work with trailing slashes', (assert) => {
166
224
 
167
225
  assert.equal(
168
226
  matchHash('/route/with/trailing/slash', routes),
169
- routes[6],
227
+ routes[7],
228
+ 'Should return the correct route object'
229
+ )
230
+
231
+ assert.equal(
232
+ matchHash('/tv/simpsons/seasons/first', routes),
233
+ routes[3],
170
234
  'Should return the correct route object'
171
235
  )
172
236
 
@@ -185,7 +249,13 @@ test('Work with and without leading slashes', (assert) => {
185
249
 
186
250
  assert.equal(
187
251
  matchHash('route/with/trailing/slash', routes),
188
- routes[6],
252
+ routes[7],
253
+ 'Should return the correct route object'
254
+ )
255
+
256
+ assert.equal(
257
+ matchHash('//tv/simpsons/seasons/first//', routes),
258
+ routes[3],
189
259
  'Should return the correct route object'
190
260
  )
191
261
 
@@ -208,7 +278,7 @@ test('Match routes case insensitive', (assert) => {
208
278
 
209
279
  assert.equal(
210
280
  matchHash('/Movies/Avengers-Endgame', routes),
211
- routes[4],
281
+ routes[5],
212
282
  'Should return the correct route object'
213
283
  )
214
284
  assert.end()
@@ -234,5 +304,170 @@ test('Match routes case insensitive, but pass props with original casing', (asse
234
304
  'Should return the correct params object'
235
305
  )
236
306
 
307
+ const match3 = matchHash('/DutchMovies/ID_123', routes)
308
+ assert.deepEqual(
309
+ match3.params,
310
+ {
311
+ id: 'ID_123',
312
+ },
313
+ 'Should return the correct params object'
314
+ )
315
+
316
+ assert.end()
317
+ })
318
+
319
+ test('Match paths with dynamic route parts along with query string params', (assert) => {
320
+ const hash1 = '#/tv/simpsons/seasons/first?token=123&ln=en'
321
+ document.location.hash = hash1
322
+
323
+ const { hash, path, queryParams } = getHash()
324
+
325
+ assert.equal(hash, hash1, 'Should return correct hash')
326
+ assert.equal(path, '/tv/simpsons/seasons/first', 'Should return the correct route path')
327
+ assert.equal(matchHash(path, routes), routes[3], 'Should return the correct route object')
328
+ assert.equal(
329
+ queryParams.get('token'),
330
+ '123',
331
+ 'Should contain the correct query parameter key with value'
332
+ )
333
+ assert.equal(
334
+ queryParams.get('ln'),
335
+ 'en',
336
+ 'Should contain the correct query parameter key with value'
337
+ )
338
+ assert.end()
339
+ })
340
+
341
+ test('Get the hash from the URL', (assert) => {
342
+ const hash = '#/movies/action/the-avengers'
343
+ document.location.hash = hash
344
+
345
+ const result = getHash()
346
+
347
+ assert.equal(
348
+ result.hash,
349
+ hash,
350
+ 'The result object should contain a hash key with the correct location hash'
351
+ )
352
+
353
+ assert.equal(
354
+ result.path,
355
+ '/movies/action/the-avengers',
356
+ 'The result object should contain a path key with the hash (stripped the # symbol)'
357
+ )
358
+
359
+ assert.end()
360
+ })
361
+
362
+ test('Get the hash from the URL and handle query params', (assert) => {
363
+ const hash = '#/movies/comedy/the-hangover?category=1&item=2'
364
+ document.location.hash = hash
365
+
366
+ const result = getHash()
367
+
368
+ assert.equal(
369
+ result.hash,
370
+ hash,
371
+ 'The result object should contain a hash key with the correct location hash without the query params'
372
+ )
373
+
374
+ assert.equal(
375
+ result.path,
376
+ '/movies/comedy/the-hangover',
377
+ 'The result object should contain a path key with the hash (stripped the # symbol)'
378
+ )
379
+
380
+ assert.equal(
381
+ result.queryParams instanceof URLSearchParams,
382
+ true,
383
+ 'The result object should contain a queryParams key with an URLSearchParams object'
384
+ )
385
+
386
+ assert.equal(
387
+ result.queryParams.get('category'),
388
+ '1',
389
+ 'The result object should contain a queryParams key with the correct route query param values'
390
+ )
391
+
392
+ assert.equal(
393
+ result.queryParams.get('item'),
394
+ '2',
395
+ 'The result object should contain a queryParams key with the correct route query param values'
396
+ )
397
+
398
+ assert.end()
399
+ })
400
+
401
+ test('Get route object from Match hash when navigating using to() method', (assert) => {
402
+ const hash = '/page1/subpage1'
403
+
404
+ to(hash)
405
+
406
+ const result = matchHash(hash, routes)
407
+
408
+ assert.equal(
409
+ result.path,
410
+ 'page1/subpage1',
411
+ 'The result object should contain a path key with path hash'
412
+ )
413
+ assert.equal(
414
+ Object.keys(result.params).length,
415
+ 0,
416
+ 'The results object should contain a params key with zero props'
417
+ )
418
+ assert.equal(
419
+ Object.keys(result.data).length,
420
+ 0,
421
+ 'The results object should contain a data key with zero props'
422
+ )
423
+ assert.equal(
424
+ Object.keys(result.options).length,
425
+ 0,
426
+ 'The results object should contain a options key with zero props'
427
+ )
428
+ assert.end()
429
+ })
430
+
431
+ test('Get route object from Match hash when navigating using to() method with options', (assert) => {
432
+ const hash = '/page1/subpage1'
433
+
434
+ to(hash, undefined, { keepAlive: true })
435
+
436
+ const result = matchHash(hash, routes)
437
+
438
+ assert.equal(
439
+ result.path,
440
+ 'page1/subpage1',
441
+ 'The result object should contain a path key with path hash'
442
+ )
443
+
444
+ assert.equal(
445
+ result.options.keepAlive,
446
+ true,
447
+ 'The results object should contain a options key with keep alive as True'
448
+ )
449
+
450
+ assert.end()
451
+ })
452
+
453
+ test('Get Hash from URL when navigating using to() method', (assert) => {
454
+ const hash = '#/movies/action/avengers'
455
+
456
+ to(hash)
457
+
458
+ const result = getHash()
459
+
460
+ assert.equal(
461
+ result.hash,
462
+ hash,
463
+ 'The result object key property should contain correct location hash'
464
+ )
465
+
466
+ assert.equal(
467
+ result.path,
468
+ '/movies/action/avengers',
469
+ 'The result object should contain a path key with hash without #'
470
+ )
471
+
237
472
  assert.end()
238
473
  })