From a1610146224ca14601c24c2d4997cbfcef428fd6 Mon Sep 17 00:00:00 2001
From: Francois Best <github@francoisbest.com>
Date: Sun, 8 Dec 2024 14:47:59 +0100
Subject: [PATCH 1/6] chore: Enforce editor config

---
 .vscode/settings.json | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/.vscode/settings.json b/.vscode/settings.json
index ebac3d3b1..1f95a140a 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,4 +1,6 @@
 {
+  "editor.tabSize": 2,
+  "editor.insertSpaces": true,
   "githubPullRequests.queries": [
     {
       "label": "Backlog",

From 0d0acc1be661e2ca10a06968e6ab1852d4ff1f1c Mon Sep 17 00:00:00 2001
From: Francois Best <github@francoisbest.com>
Date: Wed, 11 Dec 2024 00:31:20 +0100
Subject: [PATCH 2/6] test: urlKeys & clearOnDefault unit tests for
 useQueryStates

---
 packages/nuqs/src/useQueryStates.test.tsx | 210 ++++++++++++++++------
 1 file changed, 159 insertions(+), 51 deletions(-)

diff --git a/packages/nuqs/src/useQueryStates.test.tsx b/packages/nuqs/src/useQueryStates.test.tsx
index eda6b3623..af15ebb4f 100644
--- a/packages/nuqs/src/useQueryStates.test.tsx
+++ b/packages/nuqs/src/useQueryStates.test.tsx
@@ -1,41 +1,36 @@
 import { act, renderHook } from '@testing-library/react'
-import type { ReactNode } from 'react'
-import React from 'react'
-import { describe, expect, it } from 'vitest'
-import { NuqsTestingAdapter } from './adapters/testing'
+import { describe, expect, it, vi } from 'vitest'
+import {
+  withNuqsTestingAdapter,
+  type OnUrlUpdateFunction
+} from './adapters/testing'
 import { parseAsArrayOf, parseAsJson, parseAsString } from './parsers'
 import { useQueryStates } from './useQueryStates'
 
-function withSearchParams(
-  searchParams?: string | URLSearchParams | Record<string, string>
-) {
-  return (props: { children: ReactNode }) => (
-    <NuqsTestingAdapter searchParams={searchParams} {...props} />
-  )
-}
+describe('useQueryStates: referential equality', () => {
+  const defaults = {
+    str: 'foo',
+    obj: { initial: 'state' },
+    arr: [
+      {
+        initial: 'state'
+      }
+    ]
+  }
 
-const defaults = {
-  str: 'foo',
-  obj: { initial: 'state' },
-  arr: [
-    {
-      initial: 'state'
-    }
-  ]
-}
-
-const hook = ({ defaultValue } = { defaultValue: defaults.str }) => {
-  return useQueryStates({
-    str: parseAsString.withDefault(defaultValue),
-    obj: parseAsJson<any>(x => x).withDefault(defaults.obj),
-    arr: parseAsArrayOf(parseAsJson<any>(x => x)).withDefault(defaults.arr)
-  })
-}
+  const useTestHookWithDefaults = (
+    { defaultValue } = { defaultValue: defaults.str }
+  ) => {
+    return useQueryStates({
+      str: parseAsString.withDefault(defaultValue),
+      obj: parseAsJson<any>(x => x).withDefault(defaults.obj),
+      arr: parseAsArrayOf(parseAsJson<any>(x => x)).withDefault(defaults.arr)
+    })
+  }
 
-describe('useQueryStates', () => {
   it('should have referential equality on default values', () => {
-    const { result } = renderHook(hook, {
-      wrapper: NuqsTestingAdapter
+    const { result } = renderHook(useTestHookWithDefaults, {
+      wrapper: withNuqsTestingAdapter()
     })
     const [state] = result.current
     expect(state.str).toBe(defaults.str)
@@ -44,17 +39,17 @@ describe('useQueryStates', () => {
     expect(state.arr[0]).toBe(defaults.arr[0])
   })
 
-  it('should keep referential equality when resetting to defaults', () => {
-    const { result } = renderHook(hook, {
-      wrapper: withSearchParams({
-        str: 'foo',
-        obj: '{"hello":"world"}',
-        arr: '{"obj":true},{"arr":true}'
+  it('should keep referential equality when resetting to defaults', async () => {
+    const { result } = renderHook(useTestHookWithDefaults, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: {
+          str: 'foo',
+          obj: '{"hello":"world"}',
+          arr: '{"obj":true},{"arr":true}'
+        }
       })
     })
-    act(() => {
-      result.current[1](null)
-    })
+    await act(() => result.current[1](null))
     const [state] = result.current
     expect(state.str).toBe(defaults.str)
     expect(state.obj).toBe(defaults.obj)
@@ -62,18 +57,18 @@ describe('useQueryStates', () => {
     expect(state.arr[0]).toBe(defaults.arr[0])
   })
 
-  it('should keep referential equality when unrelated keys change', () => {
-    const { result } = renderHook(hook, {
-      wrapper: withSearchParams({
-        str: 'foo',
-        obj: '{"hello":"world"}'
-        // Keep arr as default
+  it('should keep referential equality when unrelated keys change', async () => {
+    const { result } = renderHook(useTestHookWithDefaults, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: {
+          str: 'foo',
+          obj: '{"hello":"world"}'
+          // Keep arr as default
+        }
       })
     })
     const [{ obj: initialObj, arr: initialArr }] = result.current
-    act(() => {
-      result.current[1]({ str: 'bar' })
-    })
+    await act(() => result.current[1]({ str: 'bar' }))
     const [{ str, obj, arr }] = result.current
     expect(str).toBe('bar')
     expect(obj).toBe(initialObj)
@@ -81,8 +76,8 @@ describe('useQueryStates', () => {
   })
 
   it('should keep referential equality when default changes for another key', () => {
-    const { result, rerender } = renderHook(hook, {
-      wrapper: withSearchParams()
+    const { result, rerender } = renderHook(useTestHookWithDefaults, {
+      wrapper: withNuqsTestingAdapter()
     })
     expect(result.current[0].str).toBe('foo')
     rerender({ defaultValue: 'b' })
@@ -93,3 +88,116 @@ describe('useQueryStates', () => {
     expect(state.arr[0]).toBe(defaults.arr[0])
   })
 })
+
+describe('useQueryStates: urlKeys remapping', () => {
+  it('uses the object key names by default', async () => {
+    const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
+    const useTestHook = () =>
+      useQueryStates({
+        foo: parseAsString,
+        bar: parseAsString
+      })
+    const { result } = renderHook(useTestHook, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: '?foo=init&bar=init',
+        onUrlUpdate
+      })
+    })
+    expect(result.current[0].foo).toEqual('init')
+    expect(result.current[0].bar).toEqual('init')
+    await act(() => result.current[1]({ foo: 'a', bar: 'b' }))
+    expect(onUrlUpdate).toHaveBeenCalledOnce()
+    expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('?foo=a&bar=b')
+  })
+
+  it('allows remapping keys partially', async () => {
+    const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
+    const useTestHook = () =>
+      useQueryStates(
+        {
+          foo: parseAsString,
+          bar: parseAsString
+        },
+        {
+          urlKeys: {
+            foo: 'f'
+          }
+        }
+      )
+    const { result } = renderHook(useTestHook, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: '?f=foo&bar=bar',
+        onUrlUpdate
+      })
+    })
+    expect(result.current[0].foo).toEqual('foo')
+    expect(result.current[0].bar).toEqual('bar')
+    await act(() => result.current[1]({ foo: 'a', bar: 'b' }))
+    expect(onUrlUpdate).toHaveBeenCalledOnce()
+    expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('?f=a&bar=b')
+  })
+})
+
+describe('useQueryStates: clearOnDefault', () => {
+  it('honors clearOnDefault: true by default', async () => {
+    const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
+    const useTestHook = () =>
+      useQueryStates({
+        test: parseAsString.withDefault('default')
+      })
+    const { result } = renderHook(useTestHook, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: '?test=init',
+        onUrlUpdate
+      })
+    })
+    await act(() => result.current[1]({ test: 'default' }))
+    expect(onUrlUpdate).toHaveBeenCalledOnce()
+    expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('')
+  })
+
+  it('supports clearOnDefault: false (parser level)', async () => {
+    const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
+    const useTestHook = () =>
+      useQueryStates({
+        a: parseAsString.withDefault('default').withOptions({
+          clearOnDefault: false
+        }),
+        b: parseAsString.withDefault('default')
+      })
+    const { result } = renderHook(useTestHook, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: '?a=init&b=init',
+        onUrlUpdate
+      })
+    })
+    await act(() => result.current[1]({ a: 'default', b: 'default' }))
+    expect(onUrlUpdate).toHaveBeenCalledOnce()
+    expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('?a=default')
+  })
+
+  it('supports clearOnDefault: false (hook level)', async () => {
+    const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
+    const useTestHook = () =>
+      useQueryStates(
+        {
+          a: parseAsString.withDefault('default'),
+          b: parseAsString.withDefault('default').withOptions({
+            clearOnDefault: true // overrides hook options
+          })
+        },
+        {
+          clearOnDefault: false
+        }
+      )
+    const { result } = renderHook(useTestHook, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: '?a=init&b=init',
+        onUrlUpdate
+      })
+    })
+    await act(() => result.current[1]({ a: 'default', b: 'default' }))
+    expect(onUrlUpdate).toHaveBeenCalledOnce()
+    expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('?a=default')
+  })
+})

From 1f98ffc5092e9fab95318d8b33a5029f66e6ab24 Mon Sep 17 00:00:00 2001
From: Francois Best <github@francoisbest.com>
Date: Wed, 11 Dec 2024 20:29:11 +0100
Subject: [PATCH 3/6] test: Remove useless e2e test (already unit-tested)

---
 packages/e2e/next/cypress/e2e/repro-599.cy.js | 18 -------------
 .../e2e/next/src/app/app/repro-599/page.tsx   | 27 -------------------
 2 files changed, 45 deletions(-)
 delete mode 100644 packages/e2e/next/cypress/e2e/repro-599.cy.js
 delete mode 100644 packages/e2e/next/src/app/app/repro-599/page.tsx

diff --git a/packages/e2e/next/cypress/e2e/repro-599.cy.js b/packages/e2e/next/cypress/e2e/repro-599.cy.js
deleted file mode 100644
index 01452371d..000000000
--- a/packages/e2e/next/cypress/e2e/repro-599.cy.js
+++ /dev/null
@@ -1,18 +0,0 @@
-/// <reference types="cypress" />
-
-it('Reproduction for issue #599', () => {
-  // Start without encoding for most characters
-  cy.visit(
-    '/app/repro-599?a %26b%3Fc%3Dd%23e%f%2Bg"h\'i`j<k>l(m)n*o,p.q:r;s/t=init'
-  )
-  cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
-  cy.get('input').should('have.value', 'init')
-  cy.get('p').should('have.text', 'init')
-  cy.get('button').click()
-  cy.get('input').should('have.value', 'works')
-  cy.get('p').should('have.text', 'works')
-  cy.location('search').should(
-    'eq',
-    '?a%20%26b%3Fc%3Dd%23e%f%2Bg%22h%27i`j%3Ck%3El(m)n*o,p.q:r;s/t=works'
-  )
-})
diff --git a/packages/e2e/next/src/app/app/repro-599/page.tsx b/packages/e2e/next/src/app/app/repro-599/page.tsx
deleted file mode 100644
index 80e824b51..000000000
--- a/packages/e2e/next/src/app/app/repro-599/page.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-'use client'
-
-import { parseAsString, useQueryState, useQueryStates } from 'nuqs'
-import { Suspense } from 'react'
-
-export default function Page() {
-  return (
-    <Suspense>
-      <Client />
-    </Suspense>
-  )
-}
-
-const key = 'a &b?c=d#e%f+g"h\'i`j<k>l(m)n*o,p.q:r;s/t'
-const parser = parseAsString.withDefault('')
-
-function Client() {
-  const [a, setValue] = useQueryState(key, parser)
-  const [{ [key]: b }] = useQueryStates({ [key]: parser })
-  return (
-    <>
-      <input value={a} onChange={e => setValue(e.target.value)} />
-      <p>{b}</p>
-      <button onClick={() => setValue('works')}>Test</button>
-    </>
-  )
-}

From f66cae86e6cb636923c75b76962d97b20aa285e7 Mon Sep 17 00:00:00 2001
From: Francois Best <github@francoisbest.com>
Date: Wed, 11 Dec 2024 20:55:24 +0100
Subject: [PATCH 4/6] test: Unit-test what can be

---
 packages/nuqs/src/useQueryState.test.ts       | 162 ++++++++++++++++++
 ...States.test.tsx => useQueryStates.test.ts} |  32 ++++
 2 files changed, 194 insertions(+)
 create mode 100644 packages/nuqs/src/useQueryState.test.ts
 rename packages/nuqs/src/{useQueryStates.test.tsx => useQueryStates.test.ts} (88%)

diff --git a/packages/nuqs/src/useQueryState.test.ts b/packages/nuqs/src/useQueryState.test.ts
new file mode 100644
index 000000000..99b0741ba
--- /dev/null
+++ b/packages/nuqs/src/useQueryState.test.ts
@@ -0,0 +1,162 @@
+import { act, renderHook } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+import {
+  withNuqsTestingAdapter,
+  type OnUrlUpdateFunction
+} from './adapters/testing'
+import { parseAsArrayOf, parseAsJson, parseAsString } from './parsers'
+import { useQueryState } from './useQueryState'
+
+describe('useQueryState: referential equality', () => {
+  const defaults = {
+    str: 'foo',
+    obj: { initial: 'state' },
+    arr: [
+      {
+        initial: 'state'
+      }
+    ]
+  }
+
+  const useTestHookWithDefaults = (
+    { defaultValue } = { defaultValue: defaults.str }
+  ) => {
+    const str = useQueryState('str', parseAsString.withDefault(defaultValue))
+    const obj = useQueryState(
+      'obj',
+      parseAsJson<any>(x => x).withDefault(defaults.obj)
+    )
+    const arr = useQueryState(
+      'arr',
+      parseAsArrayOf(parseAsJson<any>(x => x)).withDefault(defaults.arr)
+    )
+    return { str, obj, arr }
+  }
+
+  it('should have referential equality on default values', () => {
+    const { result } = renderHook(useTestHookWithDefaults, {
+      wrapper: withNuqsTestingAdapter()
+    })
+    const { str, obj, arr } = result.current
+    expect(str[0]).toBe(defaults.str)
+    expect(obj[0]).toBe(defaults.obj)
+    expect(arr[0]).toBe(defaults.arr)
+    expect(arr[0][0]).toBe(defaults.arr[0])
+  })
+
+  it('should keep referential equality when resetting to defaults', async () => {
+    const { result } = renderHook(useTestHookWithDefaults, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: {
+          str: 'foo',
+          obj: '{"hello":"world"}',
+          arr: '{"obj":true},{"arr":true}'
+        }
+      })
+    })
+    await act(() => {
+      const { str, arr, obj } = result.current
+      str[1](null)
+      obj[1](null)
+      return arr[1](null)
+    })
+    const { str, arr, obj } = result.current
+    expect(str[0]).toBe(defaults.str)
+    expect(obj[0]).toBe(defaults.obj)
+    expect(arr[0]).toBe(defaults.arr)
+    expect(arr[0][0]).toBe(defaults.arr[0])
+  })
+
+  it('should keep referential equality when unrelated keys change', async () => {
+    const { result } = renderHook(useTestHookWithDefaults, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: {
+          str: 'foo',
+          obj: '{"hello":"world"}'
+          // Keep arr as default
+        }
+      })
+    })
+    const initialObj = result.current.obj[0]
+    const initialArr = result.current.arr[0]
+    await act(() => {
+      const { str } = result.current
+      return str[1]('bar')
+    })
+    const { str, obj, arr } = result.current
+    expect(str[0]).toBe('bar')
+    expect(obj[0]).toBe(initialObj)
+    expect(arr[0]).toBe(initialArr)
+  })
+
+  it('should keep referential equality when default changes for another key', () => {
+    const { result, rerender } = renderHook(useTestHookWithDefaults, {
+      wrapper: withNuqsTestingAdapter()
+    })
+    expect(result.current.str[0]).toBe('foo')
+    rerender({ defaultValue: 'b' })
+    const { str, obj, arr } = result.current
+    expect(str[0]).toBe('b')
+    expect(obj[0]).toBe(defaults.obj)
+    expect(arr[0]).toBe(defaults.arr)
+    expect(arr[0][0]).toBe(defaults.arr[0])
+  })
+})
+
+describe('useQueryState: clearOnDefault', () => {
+  it('honors clearOnDefault: true by default', async () => {
+    const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
+    const { result } = renderHook(
+      () => useQueryState('test', parseAsString.withDefault('default')),
+      {
+        wrapper: withNuqsTestingAdapter({
+          searchParams: '?test=init',
+          onUrlUpdate
+        })
+      }
+    )
+    await act(() => result.current[1]('default'))
+    expect(onUrlUpdate).toHaveBeenCalledOnce()
+    expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('')
+  })
+
+  it('supports clearOnDefault: false (hook level)', async () => {
+    const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
+    const useTestHook = () =>
+      useQueryState(
+        'a',
+        parseAsString.withDefault('default').withOptions({
+          clearOnDefault: false
+        })
+      )
+    const { result } = renderHook(useTestHook, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: '?a=init',
+        onUrlUpdate
+      })
+    })
+    await act(() => result.current[1]('default'))
+    expect(onUrlUpdate).toHaveBeenCalledOnce()
+    expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('?a=default')
+  })
+
+  it('supports clearOnDefault: false (call level)', async () => {
+    const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
+    const useTestHook = () =>
+      useQueryState(
+        'a',
+        parseAsString.withDefault('default').withOptions({
+          clearOnDefault: true
+        })
+      )
+    const { result } = renderHook(useTestHook, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: '?a=init',
+        onUrlUpdate
+      })
+    })
+    await act(() => result.current[1]('default', { clearOnDefault: false }))
+    expect(onUrlUpdate).toHaveBeenCalledOnce()
+    expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('?a=default')
+  })
+})
diff --git a/packages/nuqs/src/useQueryStates.test.tsx b/packages/nuqs/src/useQueryStates.test.ts
similarity index 88%
rename from packages/nuqs/src/useQueryStates.test.tsx
rename to packages/nuqs/src/useQueryStates.test.ts
index af15ebb4f..a1b0958ef 100644
--- a/packages/nuqs/src/useQueryStates.test.tsx
+++ b/packages/nuqs/src/useQueryStates.test.ts
@@ -200,4 +200,36 @@ describe('useQueryStates: clearOnDefault', () => {
     expect(onUrlUpdate).toHaveBeenCalledOnce()
     expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('?a=default')
   })
+
+  it('supports clearOnDefault: false (call level)', async () => {
+    const onUrlUpdate = vi.fn<OnUrlUpdateFunction>()
+    const useTestHook = () =>
+      useQueryStates(
+        {
+          a: parseAsString.withDefault('default'),
+          b: parseAsString.withDefault('default').withOptions({
+            clearOnDefault: true // overrides hook options
+          })
+        },
+        {
+          clearOnDefault: false
+        }
+      )
+    const { result } = renderHook(useTestHook, {
+      wrapper: withNuqsTestingAdapter({
+        searchParams: '?a=init&b=init',
+        onUrlUpdate
+      })
+    })
+    await act(() =>
+      result.current[1](
+        { a: 'default', b: 'default' },
+        {
+          clearOnDefault: true
+        }
+      )
+    )
+    expect(onUrlUpdate).toHaveBeenCalledOnce()
+    expect(onUrlUpdate.mock.calls[0]![0].queryString).toEqual('')
+  })
 })

From a8f0c1759852ffee33f1a1fe9891d840f9b6fe65 Mon Sep 17 00:00:00 2001
From: Francois Best <github@francoisbest.com>
Date: Wed, 11 Dec 2024 21:07:16 +0100
Subject: [PATCH 5/6] chore: Enable test coverage reporting

---
 packages/nuqs/package.json |   1 +
 pnpm-lock.yaml             | 153 ++++++++++++++++++++++++++++++++++---
 2 files changed, 143 insertions(+), 11 deletions(-)

diff --git a/packages/nuqs/package.json b/packages/nuqs/package.json
index 1f353b21e..fd61df73e 100644
--- a/packages/nuqs/package.json
+++ b/packages/nuqs/package.json
@@ -156,6 +156,7 @@
     "@types/react": "catalog:react19",
     "@types/react-dom": "catalog:react19",
     "@vitejs/plugin-react": "^4.3.3",
+    "@vitest/coverage-v8": "^2.1.8",
     "next": "15.0.3",
     "react": "catalog:react19",
     "react-dom": "catalog:react19",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 59676037d..1fa3de811 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -526,9 +526,12 @@ importers:
       '@vitejs/plugin-react':
         specifier: ^4.3.3
         version: 4.3.3(vite@5.4.11(@types/node@22.9.0)(lightningcss@1.27.0)(terser@5.34.1))
+      '@vitest/coverage-v8':
+        specifier: ^2.1.8
+        version: 2.1.8(vitest@2.1.5(@types/node@22.9.0)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.34.1))
       next:
         specifier: 15.0.3
-        version: 15.0.3(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-a7bf2bd-20241110)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
+        version: 15.0.3(@babel/core@7.25.7)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
       react:
         specifier: catalog:react19
         version: 19.0.0
@@ -752,6 +755,9 @@ packages:
     resolution: {integrity: sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==}
     engines: {node: '>=6.9.0'}
 
+  '@bcoe/v8-coverage@0.2.3':
+    resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==}
+
   '@colors/colors@1.5.0':
     resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
     engines: {node: '>=0.1.90'}
@@ -1765,6 +1771,10 @@ packages:
     resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
     engines: {node: '>=12'}
 
+  '@istanbuljs/schema@0.1.3':
+    resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==}
+    engines: {node: '>=8'}
+
   '@jest/schemas@29.6.3':
     resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -3524,6 +3534,15 @@ packages:
     peerDependencies:
       vite: ^4.2.0 || ^5.0.0
 
+  '@vitest/coverage-v8@2.1.8':
+    resolution: {integrity: sha512-2Y7BPlKH18mAZYAW1tYByudlCYrQyl5RGvnnDYJKW5tCiO5qg3KSAy3XAxcxKz900a0ZXxWtKrMuZLe3lKBpJw==}
+    peerDependencies:
+      '@vitest/browser': 2.1.8
+      vitest: 2.1.8
+    peerDependenciesMeta:
+      '@vitest/browser':
+        optional: true
+
   '@vitest/expect@2.1.5':
     resolution: {integrity: sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==}
 
@@ -5471,6 +5490,9 @@ packages:
     resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==}
     engines: {node: '>=18'}
 
+  html-escaper@2.0.2:
+    resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
+
   html-void-elements@3.0.0:
     resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
 
@@ -5850,6 +5872,22 @@ packages:
     resolution: {integrity: sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==}
     engines: {node: ^18.17 || >=20.6.1}
 
+  istanbul-lib-coverage@3.2.2:
+    resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==}
+    engines: {node: '>=8'}
+
+  istanbul-lib-report@3.0.1:
+    resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==}
+    engines: {node: '>=10'}
+
+  istanbul-lib-source-maps@5.0.6:
+    resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==}
+    engines: {node: '>=10'}
+
+  istanbul-reports@3.1.7:
+    resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==}
+    engines: {node: '>=8'}
+
   iterator.prototype@1.1.3:
     resolution: {integrity: sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==}
     engines: {node: '>= 0.4'}
@@ -6221,9 +6259,6 @@ packages:
     resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
     hasBin: true
 
-  magic-string@0.30.11:
-    resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==}
-
   magic-string@0.30.12:
     resolution: {integrity: sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==}
 
@@ -6231,6 +6266,13 @@ packages:
     resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
     engines: {node: '>=12'}
 
+  magicast@0.3.5:
+    resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==}
+
+  make-dir@4.0.0:
+    resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
+    engines: {node: '>=10'}
+
   make-error@1.3.6:
     resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
 
@@ -8352,6 +8394,10 @@ packages:
     engines: {node: '>=10'}
     hasBin: true
 
+  test-exclude@7.0.1:
+    resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==}
+    engines: {node: '>=18'}
+
   text-extensions@2.4.0:
     resolution: {integrity: sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==}
     engines: {node: '>=8'}
@@ -9368,6 +9414,8 @@ snapshots:
       '@babel/helper-validator-identifier': 7.25.7
       to-fast-properties: 2.0.0
 
+  '@bcoe/v8-coverage@0.2.3': {}
+
   '@colors/colors@1.5.0':
     optional: true
 
@@ -10070,6 +10118,8 @@ snapshots:
       wrap-ansi: 8.1.0
       wrap-ansi-cjs: wrap-ansi@7.0.0
 
+  '@istanbuljs/schema@0.1.3': {}
+
   '@jest/schemas@29.6.3':
     dependencies:
       '@sinclair/typebox': 0.27.8
@@ -11414,7 +11464,7 @@ snapshots:
       estree-walker: 2.0.2
       glob: 10.4.5
       is-reference: 1.2.1
-      magic-string: 0.30.11
+      magic-string: 0.30.12
     optionalDependencies:
       rollup: 3.29.5
 
@@ -12323,6 +12373,24 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@vitest/coverage-v8@2.1.8(vitest@2.1.5(@types/node@22.9.0)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.34.1))':
+    dependencies:
+      '@ampproject/remapping': 2.3.0
+      '@bcoe/v8-coverage': 0.2.3
+      debug: 4.3.7(supports-color@8.1.1)
+      istanbul-lib-coverage: 3.2.2
+      istanbul-lib-report: 3.0.1
+      istanbul-lib-source-maps: 5.0.6
+      istanbul-reports: 3.1.7
+      magic-string: 0.30.12
+      magicast: 0.3.5
+      std-env: 3.8.0
+      test-exclude: 7.0.1
+      tinyrainbow: 1.2.0
+      vitest: 2.1.5(@types/node@22.9.0)(jsdom@25.0.1)(lightningcss@1.27.0)(terser@5.34.1)
+    transitivePeerDependencies:
+      - supports-color
+
   '@vitest/expect@2.1.5':
     dependencies:
       '@vitest/spy': 2.1.5
@@ -14771,6 +14839,8 @@ snapshots:
     dependencies:
       whatwg-encoding: 3.1.1
 
+  html-escaper@2.0.2: {}
+
   html-void-elements@3.0.0: {}
 
   htmlparser2@8.0.2:
@@ -15099,6 +15169,27 @@ snapshots:
       lodash.isstring: 4.0.1
       lodash.uniqby: 4.7.0
 
+  istanbul-lib-coverage@3.2.2: {}
+
+  istanbul-lib-report@3.0.1:
+    dependencies:
+      istanbul-lib-coverage: 3.2.2
+      make-dir: 4.0.0
+      supports-color: 7.2.0
+
+  istanbul-lib-source-maps@5.0.6:
+    dependencies:
+      '@jridgewell/trace-mapping': 0.3.25
+      debug: 4.3.7(supports-color@8.1.1)
+      istanbul-lib-coverage: 3.2.2
+    transitivePeerDependencies:
+      - supports-color
+
+  istanbul-reports@3.1.7:
+    dependencies:
+      html-escaper: 2.0.2
+      istanbul-lib-report: 3.0.1
+
   iterator.prototype@1.1.3:
     dependencies:
       define-properties: 1.2.1
@@ -15438,10 +15529,6 @@ snapshots:
 
   lz-string@1.5.0: {}
 
-  magic-string@0.30.11:
-    dependencies:
-      '@jridgewell/sourcemap-codec': 1.5.0
-
   magic-string@0.30.12:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.5.0
@@ -15450,6 +15537,16 @@ snapshots:
     dependencies:
       '@jridgewell/sourcemap-codec': 1.5.0
 
+  magicast@0.3.5:
+    dependencies:
+      '@babel/parser': 7.25.7
+      '@babel/types': 7.25.7
+      source-map-js: 1.2.1
+
+  make-dir@4.0.0:
+    dependencies:
+      semver: 7.6.3
+
   make-error@1.3.6: {}
 
   map-obj@1.0.1: {}
@@ -16393,6 +16490,32 @@ snapshots:
       react: 19.0.0
       react-dom: 19.0.0(react@19.0.0)
 
+  next@15.0.3(@babel/core@7.25.7)(@opentelemetry/api@1.9.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
+    dependencies:
+      '@next/env': 15.0.3
+      '@swc/counter': 0.1.3
+      '@swc/helpers': 0.5.13
+      busboy: 1.6.0
+      caniuse-lite: 1.0.30001667
+      postcss: 8.4.31
+      react: 19.0.0
+      react-dom: 19.0.0(react@19.0.0)
+      styled-jsx: 5.1.6(@babel/core@7.25.7)(react@19.0.0)
+    optionalDependencies:
+      '@next/swc-darwin-arm64': 15.0.3
+      '@next/swc-darwin-x64': 15.0.3
+      '@next/swc-linux-arm64-gnu': 15.0.3
+      '@next/swc-linux-arm64-musl': 15.0.3
+      '@next/swc-linux-x64-gnu': 15.0.3
+      '@next/swc-linux-x64-musl': 15.0.3
+      '@next/swc-win32-arm64-msvc': 15.0.3
+      '@next/swc-win32-x64-msvc': 15.0.3
+      '@opentelemetry/api': 1.9.0
+      sharp: 0.33.5
+    transitivePeerDependencies:
+      - '@babel/core'
+      - babel-plugin-macros
+
   next@15.0.3(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@19.0.0-beta-a7bf2bd-20241110)(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
     dependencies:
       '@next/env': 15.0.3
@@ -16403,7 +16526,7 @@ snapshots:
       postcss: 8.4.31
       react: 19.0.0
       react-dom: 19.0.0(react@19.0.0)
-      styled-jsx: 5.1.6(react@19.0.0)
+      styled-jsx: 5.1.6(@babel/core@7.25.7)(react@19.0.0)
     optionalDependencies:
       '@next/swc-darwin-arm64': 15.0.3
       '@next/swc-darwin-x64': 15.0.3
@@ -17992,10 +18115,12 @@ snapshots:
     dependencies:
       inline-style-parser: 0.2.4
 
-  styled-jsx@5.1.6(react@19.0.0):
+  styled-jsx@5.1.6(@babel/core@7.25.7)(react@19.0.0):
     dependencies:
       client-only: 0.0.1
       react: 19.0.0
+    optionalDependencies:
+      '@babel/core': 7.25.7
 
   sucrase@3.35.0:
     dependencies:
@@ -18126,6 +18251,12 @@ snapshots:
       commander: 2.20.3
       source-map-support: 0.5.21
 
+  test-exclude@7.0.1:
+    dependencies:
+      '@istanbuljs/schema': 0.1.3
+      glob: 10.4.5
+      minimatch: 9.0.5
+
   text-extensions@2.4.0: {}
 
   text-table@0.2.0: {}

From e6af1b58a59885dd65dd4c8ecc06bdd45e260110 Mon Sep 17 00:00:00 2001
From: Francois Best <github@francoisbest.com>
Date: Wed, 11 Dec 2024 21:10:03 +0100
Subject: [PATCH 6/6] test: Remove useless e2e test (already unit-tested)

---
 .../e2e/next/cypress/e2e/clearOnDefault.cy.js | 10 ----
 .../next/src/app/app/clearOnDefault/page.tsx  | 60 -------------------
 2 files changed, 70 deletions(-)
 delete mode 100644 packages/e2e/next/cypress/e2e/clearOnDefault.cy.js
 delete mode 100644 packages/e2e/next/src/app/app/clearOnDefault/page.tsx

diff --git a/packages/e2e/next/cypress/e2e/clearOnDefault.cy.js b/packages/e2e/next/cypress/e2e/clearOnDefault.cy.js
deleted file mode 100644
index 942322b8f..000000000
--- a/packages/e2e/next/cypress/e2e/clearOnDefault.cy.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/// <reference types="cypress" />
-
-it('Clears the URL when setting the default value when `clearOnDefault` is used', () => {
-  cy.visit(
-    '/app/clearOnDefault?a=a&b=b&array=1,2,3&json-ref={"egg":"spam"}&json-new={"egg":"spam"}&keepMe=init'
-  )
-  cy.contains('#hydration-marker', 'hydrated').should('be.hidden')
-  cy.get('button').click()
-  cy.location('search').should('eq', '?a=&keepMe=')
-})
diff --git a/packages/e2e/next/src/app/app/clearOnDefault/page.tsx b/packages/e2e/next/src/app/app/clearOnDefault/page.tsx
deleted file mode 100644
index 42f5df888..000000000
--- a/packages/e2e/next/src/app/app/clearOnDefault/page.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-'use client'
-
-import {
-  parseAsArrayOf,
-  parseAsInteger,
-  parseAsJson,
-  parseAsString,
-  useQueryState
-} from 'nuqs'
-import { Suspense } from 'react'
-
-export default function Page() {
-  return (
-    <Suspense>
-      <Client />
-    </Suspense>
-  )
-}
-
-const defaultJSON = { foo: 'bar' }
-const runtimePassthrough = (x: unknown) => x
-
-function Client() {
-  const [, setA] = useQueryState('a')
-  const [, setB] = useQueryState('b', {
-    defaultValue: ''
-  })
-  const [, setArray] = useQueryState(
-    'array',
-    parseAsArrayOf(parseAsInteger).withDefault([])
-  )
-  const [, setJsonRef] = useQueryState(
-    'json-ref',
-    parseAsJson(runtimePassthrough).withDefault(defaultJSON)
-  )
-  const [, setJsonNew] = useQueryState(
-    'json-new',
-    parseAsJson(runtimePassthrough).withDefault(defaultJSON)
-  )
-  const [, keepMe] = useQueryState(
-    'keepMe',
-    parseAsString.withDefault('').withOptions({ clearOnDefault: false })
-  )
-  return (
-    <>
-      <button
-        onClick={() => {
-          setA('')
-          setB('')
-          setArray([])
-          setJsonRef(defaultJSON)
-          setJsonNew({ ...defaultJSON })
-          keepMe('')
-        }}
-      >
-        Clear
-      </button>
-    </>
-  )
-}