]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
test: add unit test for encoding/decoding
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 5 Feb 2020 13:58:13 +0000 (14:58 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 5 Feb 2020 13:58:13 +0000 (14:58 +0100)
__tests__/url-encoding.spec.ts
src/utils/query.ts [new file with mode: 0644]

index 22405259d9d73f260d3c9a93f1c421295b513100..d54c1c5d6b121a7afc37ca96ba21a5577ed4c3a1 100644 (file)
@@ -72,30 +72,69 @@ describe('URL Encoding', () => {
     ])
   })
 
-  it('calls decodeParam with a path', async () => {
+  it('calls decode with a path', async () => {
     const router = createRouter()
-    await router.push({ name: 'repeat', params: { p: ['foo', 'bar'] } })
-    expect(encoding.encodeParam).toHaveBeenCalledTimes(2)
-    expect(encoding.encodeParam).toHaveBeenNthCalledWith(1, 'foo', 0, [
-      'foo',
-      'bar',
-    ])
-    expect(encoding.encodeParam).toHaveBeenNthCalledWith(2, 'bar', 1, [
-      'foo',
-      'bar',
-    ])
+    await router.push('/p/foo')
+    expect(encoding.decode).toHaveBeenCalledTimes(1)
+    expect(encoding.decode).toHaveBeenNthCalledWith(1, 'foo')
   })
 
-  describe.skip('resolving locations', () => {
-    it('encodes params when resolving', async () => {
-      const router = createRouter()
-      await router.push({ name: 'params', params: { p: '%€' } })
-      const currentRoute = router.currentRoute.value
-      expect(currentRoute.path).toBe(encodeURI('/p/%€'))
-      expect(currentRoute.fullPath).toBe(encodeURI('/p/%€'))
-      expect(currentRoute.query).toEqual({
-        p: '%€',
-      })
+  it('calls decode with a path with repeatable params', async () => {
+    const router = createRouter()
+    await router.push('/p/foo/bar')
+    expect(encoding.decode).toHaveBeenCalledTimes(2)
+    expect(encoding.decode).toHaveBeenNthCalledWith(1, 'foo', 0, ['foo', 'bar'])
+    expect(encoding.decode).toHaveBeenNthCalledWith(2, 'bar', 1, ['foo', 'bar'])
+  })
+
+  it('keeps decoded values in params', async () => {
+    // @ts-ignore: override to make the difference
+    encoding.decode = () => 'd'
+    // @ts-ignore
+    encoding.encodeParam = () => 'e'
+    const router = createRouter()
+    await router.push({ name: 'params', params: { p: '%' } })
+    expect(router.currentRoute.value).toMatchObject({
+      fullPath: '/p/e',
+      params: { p: '%' },
+    })
+  })
+
+  it('calls encodeQueryProperty with query', async () => {
+    const router = createRouter()
+    await router.push({ name: 'home', query: { p: 'foo' } })
+    expect(encoding.encodeQueryProperty).toHaveBeenCalledTimes(2)
+    expect(encoding.encodeQueryProperty).toHaveBeenNthCalledWith(1, 'p')
+    expect(encoding.encodeQueryProperty).toHaveBeenNthCalledWith(2, 'foo')
+  })
+
+  it('calls decode with query', async () => {
+    const router = createRouter()
+    await router.push('/?p=foo')
+    expect(encoding.decode).toHaveBeenCalledTimes(2)
+    expect(encoding.decode).toHaveBeenNthCalledWith(1, 'p')
+    expect(encoding.decode).toHaveBeenNthCalledWith(2, 'foo')
+  })
+
+  it('calls encodeQueryProperty with arrays in query', async () => {
+    const router = createRouter()
+    await router.push({ name: 'home', query: { p: ['foo', 'bar'] } })
+    expect(encoding.encodeQueryProperty).toHaveBeenCalledTimes(3)
+    expect(encoding.encodeQueryProperty).toHaveBeenNthCalledWith(1, 'p')
+    expect(encoding.encodeQueryProperty).toHaveBeenNthCalledWith(2, 'foo')
+    expect(encoding.encodeQueryProperty).toHaveBeenNthCalledWith(3, 'bar')
+  })
+
+  it('keeps decoded values in query', async () => {
+    // @ts-ignore: override to make the difference
+    encoding.decode = () => 'd'
+    // @ts-ignore
+    encoding.encodeQueryProperty = () => 'e'
+    const router = createRouter()
+    await router.push({ name: 'home', query: { p: '%' } })
+    expect(router.currentRoute.value).toMatchObject({
+      fullPath: '/?e=e',
+      query: { p: '%' },
     })
   })
 })
diff --git a/src/utils/query.ts b/src/utils/query.ts
new file mode 100644 (file)
index 0000000..0ceb9c8
--- /dev/null
@@ -0,0 +1,100 @@
+import { decode, encodeQueryProperty } from '../utils/encoding'
+
+type HistoryQueryValue = string | null
+type RawHistoryQueryValue = HistoryQueryValue | number | undefined
+export type HistoryQuery = Record<
+  string,
+  HistoryQueryValue | HistoryQueryValue[]
+>
+export type RawHistoryQuery = Record<
+  string | number,
+  RawHistoryQueryValue | RawHistoryQueryValue[]
+>
+
+/**
+ * Transform a queryString into a query object. Accept both, a version with the leading `?` and without
+ * Should work as URLSearchParams
+ * @param search
+ * @returns a query object
+ */
+export function parseQuery(search: string): HistoryQuery {
+  const query: HistoryQuery = {}
+  // avoid creating an object with an empty key and empty value
+  // because of split('&')
+  if (search === '' || search === '?') return query
+  const hasLeadingIM = search[0] === '?'
+  const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&')
+  for (let i = 0; i < searchParams.length; ++i) {
+    let [key, rawValue] = searchParams[i].split('=') as [
+      string,
+      string | undefined
+    ]
+    key = decode(key)
+    // avoid decoding null
+    let value = rawValue == null ? null : decode(rawValue)
+    if (key in query) {
+      // an extra variable for ts types
+      let currentValue = query[key]
+      if (!Array.isArray(currentValue)) {
+        currentValue = query[key] = [currentValue]
+      }
+      currentValue.push(value)
+    } else {
+      query[key] = value
+    }
+  }
+  return query
+}
+
+/**
+ * Stringify an object query. Works like URLSearchParams. Doesn't prepend a `?`
+ * @param query
+ */
+export function stringifyQuery(query: RawHistoryQuery): string {
+  let search = ''
+  for (let key in query) {
+    if (search.length) search += '&'
+    const value = query[key]
+    key = encodeQueryProperty(key)
+    if (value == null) {
+      // only null adds the value
+      if (value !== undefined) search += key
+      continue
+    }
+    // keep null values
+    let values: RawHistoryQueryValue[] = Array.isArray(value)
+      ? value.map(v => v && encodeQueryProperty(v))
+      : [value && encodeQueryProperty(value)]
+
+    for (let i = 0; i < values.length; i++) {
+      // only append & with i > 0
+      search += (i ? '&' : '') + key
+      if (values[i] != null) search += ('=' + values[i]) as string
+    }
+  }
+
+  return search
+}
+
+/**
+ * Transforms a RawQuery intoe a NormalizedQuery by casting numbers into
+ * strings, removing keys with an undefined value and replacing undefined with
+ * null in arrays
+ * @param query
+ */
+export function normalizeQuery(query: RawHistoryQuery): HistoryQuery {
+  const normalizedQuery: HistoryQuery = {}
+
+  for (let key in query) {
+    let value = query[key]
+    if (value !== undefined) {
+      normalizedQuery[key] = Array.isArray(value)
+        ? value.map(v => (v == null ? null : '' + v))
+        : value == null
+        ? value
+        : '' + value
+    }
+  }
+
+  return normalizedQuery
+}