stringify.js 52 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306
  1. 'use strict';
  2. var test = require('tape');
  3. var qs = require('../');
  4. var utils = require('../lib/utils');
  5. var iconv = require('iconv-lite');
  6. var SaferBuffer = require('safer-buffer').Buffer;
  7. var hasSymbols = require('has-symbols');
  8. var mockProperty = require('mock-property');
  9. var emptyTestCases = require('./empty-keys-cases').emptyTestCases;
  10. var hasProto = require('has-proto')();
  11. var hasBigInt = require('has-bigints')();
  12. test('stringify()', function (t) {
  13. t.test('stringifies a querystring object', function (st) {
  14. st.equal(qs.stringify({ a: 'b' }), 'a=b');
  15. st.equal(qs.stringify({ a: 1 }), 'a=1');
  16. st.equal(qs.stringify({ a: 1, b: 2 }), 'a=1&b=2');
  17. st.equal(qs.stringify({ a: 'A_Z' }), 'a=A_Z');
  18. st.equal(qs.stringify({ a: '€' }), 'a=%E2%82%AC');
  19. st.equal(qs.stringify({ a: '' }), 'a=%EE%80%80');
  20. st.equal(qs.stringify({ a: 'א' }), 'a=%D7%90');
  21. st.equal(qs.stringify({ a: '𐐷' }), 'a=%F0%90%90%B7');
  22. st.end();
  23. });
  24. t.test('stringifies falsy values', function (st) {
  25. st.equal(qs.stringify(undefined), '');
  26. st.equal(qs.stringify(null), '');
  27. st.equal(qs.stringify(null, { strictNullHandling: true }), '');
  28. st.equal(qs.stringify(false), '');
  29. st.equal(qs.stringify(0), '');
  30. st.end();
  31. });
  32. t.test('stringifies symbols', { skip: !hasSymbols() }, function (st) {
  33. st.equal(qs.stringify(Symbol.iterator), '');
  34. st.equal(qs.stringify([Symbol.iterator]), '0=Symbol%28Symbol.iterator%29');
  35. st.equal(qs.stringify({ a: Symbol.iterator }), 'a=Symbol%28Symbol.iterator%29');
  36. st.equal(
  37. qs.stringify({ a: [Symbol.iterator] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
  38. 'a[]=Symbol%28Symbol.iterator%29'
  39. );
  40. st.end();
  41. });
  42. t.test('stringifies bigints', { skip: !hasBigInt }, function (st) {
  43. var three = BigInt(3);
  44. var encodeWithN = function (value, defaultEncoder, charset) {
  45. var result = defaultEncoder(value, defaultEncoder, charset);
  46. return typeof value === 'bigint' ? result + 'n' : result;
  47. };
  48. st.equal(qs.stringify(three), '');
  49. st.equal(qs.stringify([three]), '0=3');
  50. st.equal(qs.stringify([three], { encoder: encodeWithN }), '0=3n');
  51. st.equal(qs.stringify({ a: three }), 'a=3');
  52. st.equal(qs.stringify({ a: three }, { encoder: encodeWithN }), 'a=3n');
  53. st.equal(
  54. qs.stringify({ a: [three] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
  55. 'a[]=3'
  56. );
  57. st.equal(
  58. qs.stringify({ a: [three] }, { encodeValuesOnly: true, encoder: encodeWithN, arrayFormat: 'brackets' }),
  59. 'a[]=3n'
  60. );
  61. st.end();
  62. });
  63. t.test('encodes dot in key of object when encodeDotInKeys and allowDots is provided', function (st) {
  64. st.equal(
  65. qs.stringify(
  66. { 'name.obj': { first: 'John', last: 'Doe' } },
  67. { allowDots: false, encodeDotInKeys: false }
  68. ),
  69. 'name.obj%5Bfirst%5D=John&name.obj%5Blast%5D=Doe',
  70. 'with allowDots false and encodeDotInKeys false'
  71. );
  72. st.equal(
  73. qs.stringify(
  74. { 'name.obj': { first: 'John', last: 'Doe' } },
  75. { allowDots: true, encodeDotInKeys: false }
  76. ),
  77. 'name.obj.first=John&name.obj.last=Doe',
  78. 'with allowDots true and encodeDotInKeys false'
  79. );
  80. st.equal(
  81. qs.stringify(
  82. { 'name.obj': { first: 'John', last: 'Doe' } },
  83. { allowDots: false, encodeDotInKeys: true }
  84. ),
  85. 'name%252Eobj%5Bfirst%5D=John&name%252Eobj%5Blast%5D=Doe',
  86. 'with allowDots false and encodeDotInKeys true'
  87. );
  88. st.equal(
  89. qs.stringify(
  90. { 'name.obj': { first: 'John', last: 'Doe' } },
  91. { allowDots: true, encodeDotInKeys: true }
  92. ),
  93. 'name%252Eobj.first=John&name%252Eobj.last=Doe',
  94. 'with allowDots true and encodeDotInKeys true'
  95. );
  96. st.equal(
  97. qs.stringify(
  98. { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
  99. { allowDots: false, encodeDotInKeys: false }
  100. ),
  101. 'name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe',
  102. 'with allowDots false and encodeDotInKeys false'
  103. );
  104. st.equal(
  105. qs.stringify(
  106. { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
  107. { allowDots: true, encodeDotInKeys: false }
  108. ),
  109. 'name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe',
  110. 'with allowDots false and encodeDotInKeys false'
  111. );
  112. st.equal(
  113. qs.stringify(
  114. { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
  115. { allowDots: false, encodeDotInKeys: true }
  116. ),
  117. 'name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe',
  118. 'with allowDots false and encodeDotInKeys true'
  119. );
  120. st.equal(
  121. qs.stringify(
  122. { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
  123. { allowDots: true, encodeDotInKeys: true }
  124. ),
  125. 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
  126. 'with allowDots true and encodeDotInKeys true'
  127. );
  128. st.end();
  129. });
  130. t.test('should encode dot in key of object, and automatically set allowDots to `true` when encodeDotInKeys is true and allowDots in undefined', function (st) {
  131. st.equal(
  132. qs.stringify(
  133. { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } },
  134. { encodeDotInKeys: true }
  135. ),
  136. 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe',
  137. 'with allowDots undefined and encodeDotInKeys true'
  138. );
  139. st.end();
  140. });
  141. t.test('should encode dot in key of object when encodeDotInKeys and allowDots is provided, and nothing else when encodeValuesOnly is provided', function (st) {
  142. st.equal(
  143. qs.stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, {
  144. encodeDotInKeys: true, allowDots: true, encodeValuesOnly: true
  145. }),
  146. 'name%2Eobj.first=John&name%2Eobj.last=Doe'
  147. );
  148. st.equal(
  149. qs.stringify({ 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }),
  150. 'name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe'
  151. );
  152. st.end();
  153. });
  154. t.test('throws when `commaRoundTrip` is not a boolean', function (st) {
  155. st['throws'](
  156. function () { qs.stringify({}, { commaRoundTrip: 'not a boolean' }); },
  157. TypeError,
  158. 'throws when `commaRoundTrip` is not a boolean'
  159. );
  160. st.end();
  161. });
  162. t.test('throws when `encodeDotInKeys` is not a boolean', function (st) {
  163. st['throws'](
  164. function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); },
  165. TypeError
  166. );
  167. st['throws'](
  168. function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); },
  169. TypeError
  170. );
  171. st['throws'](
  172. function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); },
  173. TypeError
  174. );
  175. st['throws'](
  176. function () { qs.stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); },
  177. TypeError
  178. );
  179. st.end();
  180. });
  181. t.test('adds query prefix', function (st) {
  182. st.equal(qs.stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b');
  183. st.end();
  184. });
  185. t.test('with query prefix, outputs blank string given an empty object', function (st) {
  186. st.equal(qs.stringify({}, { addQueryPrefix: true }), '');
  187. st.end();
  188. });
  189. t.test('stringifies nested falsy values', function (st) {
  190. st.equal(qs.stringify({ a: { b: { c: null } } }), 'a%5Bb%5D%5Bc%5D=');
  191. st.equal(qs.stringify({ a: { b: { c: null } } }, { strictNullHandling: true }), 'a%5Bb%5D%5Bc%5D');
  192. st.equal(qs.stringify({ a: { b: { c: false } } }), 'a%5Bb%5D%5Bc%5D=false');
  193. st.end();
  194. });
  195. t.test('stringifies a nested object', function (st) {
  196. st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
  197. st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }), 'a%5Bb%5D%5Bc%5D%5Bd%5D=e');
  198. st.end();
  199. });
  200. t.test('`allowDots` option: stringifies a nested object with dots notation', function (st) {
  201. st.equal(qs.stringify({ a: { b: 'c' } }, { allowDots: true }), 'a.b=c');
  202. st.equal(qs.stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true }), 'a.b.c.d=e');
  203. st.end();
  204. });
  205. t.test('stringifies an array value', function (st) {
  206. st.equal(
  207. qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' }),
  208. 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d',
  209. 'indices => indices'
  210. );
  211. st.equal(
  212. qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' }),
  213. 'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d',
  214. 'brackets => brackets'
  215. );
  216. st.equal(
  217. qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' }),
  218. 'a=b%2Cc%2Cd',
  219. 'comma => comma'
  220. );
  221. st.equal(
  222. qs.stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true }),
  223. 'a=b%2Cc%2Cd',
  224. 'comma round trip => comma'
  225. );
  226. st.equal(
  227. qs.stringify({ a: ['b', 'c', 'd'] }),
  228. 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d',
  229. 'default => indices'
  230. );
  231. st.end();
  232. });
  233. t.test('`skipNulls` option', function (st) {
  234. st.equal(
  235. qs.stringify({ a: 'b', c: null }, { skipNulls: true }),
  236. 'a=b',
  237. 'omits nulls when asked'
  238. );
  239. st.equal(
  240. qs.stringify({ a: { b: 'c', d: null } }, { skipNulls: true }),
  241. 'a%5Bb%5D=c',
  242. 'omits nested nulls when asked'
  243. );
  244. st.end();
  245. });
  246. t.test('omits array indices when asked', function (st) {
  247. st.equal(qs.stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d');
  248. st.end();
  249. });
  250. t.test('omits object key/value pair when value is empty array', function (st) {
  251. st.equal(qs.stringify({ a: [], b: 'zz' }), 'b=zz');
  252. st.end();
  253. });
  254. t.test('should not omit object key/value pair when value is empty array and when asked', function (st) {
  255. st.equal(qs.stringify({ a: [], b: 'zz' }), 'b=zz');
  256. st.equal(qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false }), 'b=zz');
  257. st.equal(qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true }), 'a[]&b=zz');
  258. st.end();
  259. });
  260. t.test('should throw when allowEmptyArrays is not of type boolean', function (st) {
  261. st['throws'](
  262. function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); },
  263. TypeError
  264. );
  265. st['throws'](
  266. function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); },
  267. TypeError
  268. );
  269. st['throws'](
  270. function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); },
  271. TypeError
  272. );
  273. st['throws'](
  274. function () { qs.stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); },
  275. TypeError
  276. );
  277. st.end();
  278. });
  279. t.test('allowEmptyArrays + strictNullHandling', function (st) {
  280. st.equal(
  281. qs.stringify(
  282. { testEmptyArray: [] },
  283. { strictNullHandling: true, allowEmptyArrays: true }
  284. ),
  285. 'testEmptyArray[]'
  286. );
  287. st.end();
  288. });
  289. t.test('stringifies an array value with one item vs multiple items', function (st) {
  290. st.test('non-array item', function (s2t) {
  291. s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a=c');
  292. s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a=c');
  293. s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c');
  294. s2t.equal(qs.stringify({ a: 'c' }, { encodeValuesOnly: true }), 'a=c');
  295. s2t.end();
  296. });
  297. st.test('array with a single item', function (s2t) {
  298. s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[0]=c');
  299. s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=c');
  300. s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c');
  301. s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), 'a[]=c'); // so it parses back as an array
  302. s2t.equal(qs.stringify({ a: ['c'] }, { encodeValuesOnly: true }), 'a[0]=c');
  303. s2t.end();
  304. });
  305. st.test('array with multiple items', function (s2t) {
  306. s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[0]=c&a[1]=d');
  307. s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=c&a[]=d');
  308. s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c,d');
  309. s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), 'a=c,d');
  310. s2t.equal(qs.stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true }), 'a[0]=c&a[1]=d');
  311. s2t.end();
  312. });
  313. st.test('array with multiple items with a comma inside', function (s2t) {
  314. s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c%2Cd,e');
  315. s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' }), 'a=c%2Cd%2Ce');
  316. s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), 'a=c%2Cd,e');
  317. s2t.equal(qs.stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true }), 'a=c%2Cd%2Ce');
  318. s2t.end();
  319. });
  320. st.end();
  321. });
  322. t.test('stringifies a nested array value', function (st) {
  323. st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[b][0]=c&a[b][1]=d');
  324. st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[b][]=c&a[b][]=d');
  325. st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a[b]=c,d');
  326. st.equal(qs.stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true }), 'a[b][0]=c&a[b][1]=d');
  327. st.end();
  328. });
  329. t.test('stringifies comma and empty array values', function (st) {
  330. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'indices' }), 'a[0]=,&a[1]=&a[2]=c,d%');
  331. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'brackets' }), 'a[]=,&a[]=&a[]=c,d%');
  332. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'comma' }), 'a=,,,c,d%');
  333. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'repeat' }), 'a=,&a=&a=c,d%');
  334. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[0]=%2C&a[1]=&a[2]=c%2Cd%25');
  335. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=%2C&a[]=&a[]=c%2Cd%25');
  336. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=%2C,,c%2Cd%25');
  337. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a=%2C&a=&a=c%2Cd%25');
  338. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), 'a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25');
  339. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }), 'a%5B%5D=%2C&a%5B%5D=&a%5B%5D=c%2Cd%25');
  340. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), 'a=%2C%2C%2Cc%2Cd%25');
  341. st.equal(qs.stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), 'a=%2C&a=&a=c%2Cd%25');
  342. st.end();
  343. });
  344. t.test('stringifies comma and empty non-array values', function (st) {
  345. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' }), 'a=,&b=&c=c,d%');
  346. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' }), 'a=,&b=&c=c,d%');
  347. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'comma' }), 'a=,&b=&c=c,d%');
  348. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'repeat' }), 'a=,&b=&c=c,d%');
  349. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }), 'a=%2C&b=&c=c%2Cd%25');
  350. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a=%2C&b=&c=c%2Cd%25');
  351. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=%2C&b=&c=c%2Cd%25');
  352. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a=%2C&b=&c=c%2Cd%25');
  353. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), 'a=%2C&b=&c=c%2Cd%25');
  354. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }), 'a=%2C&b=&c=c%2Cd%25');
  355. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), 'a=%2C&b=&c=c%2Cd%25');
  356. st.equal(qs.stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), 'a=%2C&b=&c=c%2Cd%25');
  357. st.end();
  358. });
  359. t.test('stringifies a nested array value with dots notation', function (st) {
  360. st.equal(
  361. qs.stringify(
  362. { a: { b: ['c', 'd'] } },
  363. { allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }
  364. ),
  365. 'a.b[0]=c&a.b[1]=d',
  366. 'indices: stringifies with dots + indices'
  367. );
  368. st.equal(
  369. qs.stringify(
  370. { a: { b: ['c', 'd'] } },
  371. { allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }
  372. ),
  373. 'a.b[]=c&a.b[]=d',
  374. 'brackets: stringifies with dots + brackets'
  375. );
  376. st.equal(
  377. qs.stringify(
  378. { a: { b: ['c', 'd'] } },
  379. { allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }
  380. ),
  381. 'a.b=c,d',
  382. 'comma: stringifies with dots + comma'
  383. );
  384. st.equal(
  385. qs.stringify(
  386. { a: { b: ['c', 'd'] } },
  387. { allowDots: true, encodeValuesOnly: true }
  388. ),
  389. 'a.b[0]=c&a.b[1]=d',
  390. 'default: stringifies with dots + indices'
  391. );
  392. st.end();
  393. });
  394. t.test('stringifies an object inside an array', function (st) {
  395. st.equal(
  396. qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true }),
  397. 'a[0][b]=c',
  398. 'indices => indices'
  399. );
  400. st.equal(
  401. qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }),
  402. 'a[b]=c',
  403. 'repeat => repeat'
  404. );
  405. st.equal(
  406. qs.stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }),
  407. 'a[][b]=c',
  408. 'brackets => brackets'
  409. );
  410. st.equal(
  411. qs.stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true }),
  412. 'a[0][b]=c',
  413. 'default => indices'
  414. );
  415. st.equal(
  416. qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true }),
  417. 'a[0][b][c][0]=1',
  418. 'indices => indices'
  419. );
  420. st.equal(
  421. qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }),
  422. 'a[b][c]=1',
  423. 'repeat => repeat'
  424. );
  425. st.equal(
  426. qs.stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }),
  427. 'a[][b][c][]=1',
  428. 'brackets => brackets'
  429. );
  430. st.equal(
  431. qs.stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true }),
  432. 'a[0][b][c][0]=1',
  433. 'default => indices'
  434. );
  435. st.end();
  436. });
  437. t.test('stringifies an array with mixed objects and primitives', function (st) {
  438. st.equal(
  439. qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' }),
  440. 'a[0][b]=1&a[1]=2&a[2]=3',
  441. 'indices => indices'
  442. );
  443. st.equal(
  444. qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
  445. 'a[][b]=1&a[]=2&a[]=3',
  446. 'brackets => brackets'
  447. );
  448. st.equal(
  449. qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }),
  450. '???',
  451. 'brackets => brackets',
  452. { skip: 'TODO: figure out what this should do' }
  453. );
  454. st.equal(
  455. qs.stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true }),
  456. 'a[0][b]=1&a[1]=2&a[2]=3',
  457. 'default => indices'
  458. );
  459. st.end();
  460. });
  461. t.test('stringifies an object inside an array with dots notation', function (st) {
  462. st.equal(
  463. qs.stringify(
  464. { a: [{ b: 'c' }] },
  465. { allowDots: true, encode: false, arrayFormat: 'indices' }
  466. ),
  467. 'a[0].b=c',
  468. 'indices => indices'
  469. );
  470. st.equal(
  471. qs.stringify(
  472. { a: [{ b: 'c' }] },
  473. { allowDots: true, encode: false, arrayFormat: 'brackets' }
  474. ),
  475. 'a[].b=c',
  476. 'brackets => brackets'
  477. );
  478. st.equal(
  479. qs.stringify(
  480. { a: [{ b: 'c' }] },
  481. { allowDots: true, encode: false }
  482. ),
  483. 'a[0].b=c',
  484. 'default => indices'
  485. );
  486. st.equal(
  487. qs.stringify(
  488. { a: [{ b: { c: [1] } }] },
  489. { allowDots: true, encode: false, arrayFormat: 'indices' }
  490. ),
  491. 'a[0].b.c[0]=1',
  492. 'indices => indices'
  493. );
  494. st.equal(
  495. qs.stringify(
  496. { a: [{ b: { c: [1] } }] },
  497. { allowDots: true, encode: false, arrayFormat: 'brackets' }
  498. ),
  499. 'a[].b.c[]=1',
  500. 'brackets => brackets'
  501. );
  502. st.equal(
  503. qs.stringify(
  504. { a: [{ b: { c: [1] } }] },
  505. { allowDots: true, encode: false }
  506. ),
  507. 'a[0].b.c[0]=1',
  508. 'default => indices'
  509. );
  510. st.end();
  511. });
  512. t.test('does not omit object keys when indices = false', function (st) {
  513. st.equal(qs.stringify({ a: [{ b: 'c' }] }, { indices: false }), 'a%5Bb%5D=c');
  514. st.end();
  515. });
  516. t.test('uses indices notation for arrays when indices=true', function (st) {
  517. st.equal(qs.stringify({ a: ['b', 'c'] }, { indices: true }), 'a%5B0%5D=b&a%5B1%5D=c');
  518. st.end();
  519. });
  520. t.test('uses indices notation for arrays when no arrayFormat is specified', function (st) {
  521. st.equal(qs.stringify({ a: ['b', 'c'] }), 'a%5B0%5D=b&a%5B1%5D=c');
  522. st.end();
  523. });
  524. t.test('uses indices notation for arrays when arrayFormat=indices', function (st) {
  525. st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }), 'a%5B0%5D=b&a%5B1%5D=c');
  526. st.end();
  527. });
  528. t.test('uses repeat notation for arrays when arrayFormat=repeat', function (st) {
  529. st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }), 'a=b&a=c');
  530. st.end();
  531. });
  532. t.test('uses brackets notation for arrays when arrayFormat=brackets', function (st) {
  533. st.equal(qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }), 'a%5B%5D=b&a%5B%5D=c');
  534. st.end();
  535. });
  536. t.test('stringifies a complicated object', function (st) {
  537. st.equal(qs.stringify({ a: { b: 'c', d: 'e' } }), 'a%5Bb%5D=c&a%5Bd%5D=e');
  538. st.end();
  539. });
  540. t.test('stringifies an empty value', function (st) {
  541. st.equal(qs.stringify({ a: '' }), 'a=');
  542. st.equal(qs.stringify({ a: null }, { strictNullHandling: true }), 'a');
  543. st.equal(qs.stringify({ a: '', b: '' }), 'a=&b=');
  544. st.equal(qs.stringify({ a: null, b: '' }, { strictNullHandling: true }), 'a&b=');
  545. st.equal(qs.stringify({ a: { b: '' } }), 'a%5Bb%5D=');
  546. st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: true }), 'a%5Bb%5D');
  547. st.equal(qs.stringify({ a: { b: null } }, { strictNullHandling: false }), 'a%5Bb%5D=');
  548. st.end();
  549. });
  550. t.test('stringifies an empty array in different arrayFormat', function (st) {
  551. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false }), 'b[0]=&c=c');
  552. // arrayFormat default
  553. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' }), 'b[0]=&c=c');
  554. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' }), 'b[]=&c=c');
  555. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' }), 'b=&c=c');
  556. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' }), 'b=&c=c');
  557. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', commaRoundTrip: true }), 'b[]=&c=c');
  558. // with strictNullHandling
  559. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', strictNullHandling: true }), 'b[0]&c=c');
  560. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', strictNullHandling: true }), 'b[]&c=c');
  561. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', strictNullHandling: true }), 'b&c=c');
  562. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', strictNullHandling: true }), 'b&c=c');
  563. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', strictNullHandling: true, commaRoundTrip: true }), 'b[]&c=c');
  564. // with skipNulls
  565. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', skipNulls: true }), 'c=c');
  566. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', skipNulls: true }), 'c=c');
  567. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', skipNulls: true }), 'c=c');
  568. st.equal(qs.stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', skipNulls: true }), 'c=c');
  569. st.end();
  570. });
  571. t.test('stringifies a null object', { skip: !hasProto }, function (st) {
  572. st.equal(qs.stringify({ __proto__: null, a: 'b' }), 'a=b');
  573. st.end();
  574. });
  575. t.test('returns an empty string for invalid input', function (st) {
  576. st.equal(qs.stringify(undefined), '');
  577. st.equal(qs.stringify(false), '');
  578. st.equal(qs.stringify(null), '');
  579. st.equal(qs.stringify(''), '');
  580. st.end();
  581. });
  582. t.test('stringifies an object with a null object as a child', { skip: !hasProto }, function (st) {
  583. st.equal(qs.stringify({ a: { __proto__: null, b: 'c' } }), 'a%5Bb%5D=c');
  584. st.end();
  585. });
  586. t.test('drops keys with a value of undefined', function (st) {
  587. st.equal(qs.stringify({ a: undefined }), '');
  588. st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true }), 'a%5Bc%5D');
  589. st.equal(qs.stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false }), 'a%5Bc%5D=');
  590. st.equal(qs.stringify({ a: { b: undefined, c: '' } }), 'a%5Bc%5D=');
  591. st.end();
  592. });
  593. t.test('url encodes values', function (st) {
  594. st.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
  595. st.end();
  596. });
  597. t.test('stringifies a date', function (st) {
  598. var now = new Date();
  599. var str = 'a=' + encodeURIComponent(now.toISOString());
  600. st.equal(qs.stringify({ a: now }), str);
  601. st.end();
  602. });
  603. t.test('stringifies the weird object from qs', function (st) {
  604. st.equal(qs.stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' }), 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F');
  605. st.end();
  606. });
  607. t.test('skips properties that are part of the object prototype', function (st) {
  608. st.intercept(Object.prototype, 'crash', { value: 'test' });
  609. st.equal(qs.stringify({ a: 'b' }), 'a=b');
  610. st.equal(qs.stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c');
  611. st.end();
  612. });
  613. t.test('stringifies boolean values', function (st) {
  614. st.equal(qs.stringify({ a: true }), 'a=true');
  615. st.equal(qs.stringify({ a: { b: true } }), 'a%5Bb%5D=true');
  616. st.equal(qs.stringify({ b: false }), 'b=false');
  617. st.equal(qs.stringify({ b: { c: false } }), 'b%5Bc%5D=false');
  618. st.end();
  619. });
  620. t.test('stringifies buffer values', function (st) {
  621. st.equal(qs.stringify({ a: SaferBuffer.from('test') }), 'a=test');
  622. st.equal(qs.stringify({ a: { b: SaferBuffer.from('test') } }), 'a%5Bb%5D=test');
  623. st.end();
  624. });
  625. t.test('stringifies an object using an alternative delimiter', function (st) {
  626. st.equal(qs.stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d');
  627. st.end();
  628. });
  629. t.test('does not blow up when Buffer global is missing', function (st) {
  630. var restore = mockProperty(global, 'Buffer', { 'delete': true });
  631. var result = qs.stringify({ a: 'b', c: 'd' });
  632. restore();
  633. st.equal(result, 'a=b&c=d');
  634. st.end();
  635. });
  636. t.test('does not crash when parsing circular references', function (st) {
  637. var a = {};
  638. a.b = a;
  639. st['throws'](
  640. function () { qs.stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); },
  641. /RangeError: Cyclic object value/,
  642. 'cyclic values throw'
  643. );
  644. var circular = {
  645. a: 'value'
  646. };
  647. circular.a = circular;
  648. st['throws'](
  649. function () { qs.stringify(circular); },
  650. /RangeError: Cyclic object value/,
  651. 'cyclic values throw'
  652. );
  653. var arr = ['a'];
  654. st.doesNotThrow(
  655. function () { qs.stringify({ x: arr, y: arr }); },
  656. 'non-cyclic values do not throw'
  657. );
  658. st.end();
  659. });
  660. t.test('non-circular duplicated references can still work', function (st) {
  661. var hourOfDay = {
  662. 'function': 'hour_of_day'
  663. };
  664. var p1 = {
  665. 'function': 'gte',
  666. arguments: [hourOfDay, 0]
  667. };
  668. var p2 = {
  669. 'function': 'lte',
  670. arguments: [hourOfDay, 23]
  671. };
  672. st.equal(
  673. qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }),
  674. 'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23'
  675. );
  676. st.equal(
  677. qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }),
  678. 'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23'
  679. );
  680. st.equal(
  681. qs.stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'repeat' }),
  682. 'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23'
  683. );
  684. st.end();
  685. });
  686. t.test('selects properties when filter=array', function (st) {
  687. st.equal(qs.stringify({ a: 'b' }, { filter: ['a'] }), 'a=b');
  688. st.equal(qs.stringify({ a: 1 }, { filter: [] }), '');
  689. st.equal(
  690. qs.stringify(
  691. { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
  692. { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }
  693. ),
  694. 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3',
  695. 'indices => indices'
  696. );
  697. st.equal(
  698. qs.stringify(
  699. { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
  700. { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }
  701. ),
  702. 'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3',
  703. 'brackets => brackets'
  704. );
  705. st.equal(
  706. qs.stringify(
  707. { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' },
  708. { filter: ['a', 'b', 0, 2] }
  709. ),
  710. 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3',
  711. 'default => indices'
  712. );
  713. st.end();
  714. });
  715. t.test('supports custom representations when filter=function', function (st) {
  716. var calls = 0;
  717. var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } };
  718. var filterFunc = function (prefix, value) {
  719. calls += 1;
  720. if (calls === 1) {
  721. st.equal(prefix, '', 'prefix is empty');
  722. st.equal(value, obj);
  723. } else if (prefix === 'c') {
  724. return void 0;
  725. } else if (value instanceof Date) {
  726. st.equal(prefix, 'e[f]');
  727. return value.getTime();
  728. }
  729. return value;
  730. };
  731. st.equal(qs.stringify(obj, { filter: filterFunc }), 'a=b&e%5Bf%5D=1257894000000');
  732. st.equal(calls, 5);
  733. st.end();
  734. });
  735. t.test('can disable uri encoding', function (st) {
  736. st.equal(qs.stringify({ a: 'b' }, { encode: false }), 'a=b');
  737. st.equal(qs.stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c');
  738. st.equal(qs.stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false }), 'a=b&c');
  739. st.end();
  740. });
  741. t.test('can sort the keys', function (st) {
  742. var sort = function (a, b) {
  743. return a.localeCompare(b);
  744. };
  745. st.equal(qs.stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort }), 'a=c&b=f&z=y');
  746. st.equal(qs.stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort }), 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a');
  747. st.end();
  748. });
  749. t.test('can sort the keys at depth 3 or more too', function (st) {
  750. var sort = function (a, b) {
  751. return a.localeCompare(b);
  752. };
  753. st.equal(
  754. qs.stringify(
  755. { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' },
  756. { sort: sort, encode: false }
  757. ),
  758. 'a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb'
  759. );
  760. st.equal(
  761. qs.stringify(
  762. { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' },
  763. { sort: null, encode: false }
  764. ),
  765. 'a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b'
  766. );
  767. st.end();
  768. });
  769. t.test('can stringify with custom encoding', function (st) {
  770. st.equal(qs.stringify({ 県: '大阪府', '': '' }, {
  771. encoder: function (str) {
  772. if (str.length === 0) {
  773. return '';
  774. }
  775. var buf = iconv.encode(str, 'shiftjis');
  776. var result = [];
  777. for (var i = 0; i < buf.length; ++i) {
  778. result.push(buf.readUInt8(i).toString(16));
  779. }
  780. return '%' + result.join('%');
  781. }
  782. }), '%8c%a7=%91%e5%8d%e3%95%7b&=');
  783. st.end();
  784. });
  785. t.test('receives the default encoder as a second argument', function (st) {
  786. st.plan(8);
  787. qs.stringify({ a: 1, b: new Date(), c: true, d: [1] }, {
  788. encoder: function (str) {
  789. st.match(typeof str, /^(?:string|number|boolean)$/);
  790. return '';
  791. }
  792. });
  793. st.end();
  794. });
  795. t.test('receives the default encoder as a second argument', function (st) {
  796. st.plan(2);
  797. qs.stringify({ a: 1 }, {
  798. encoder: function (str, defaultEncoder) {
  799. st.equal(defaultEncoder, utils.encode);
  800. }
  801. });
  802. st.end();
  803. });
  804. t.test('throws error with wrong encoder', function (st) {
  805. st['throws'](function () {
  806. qs.stringify({}, { encoder: 'string' });
  807. }, new TypeError('Encoder has to be a function.'));
  808. st.end();
  809. });
  810. t.test('can use custom encoder for a buffer object', { skip: typeof Buffer === 'undefined' }, function (st) {
  811. st.equal(qs.stringify({ a: SaferBuffer.from([1]) }, {
  812. encoder: function (buffer) {
  813. if (typeof buffer === 'string') {
  814. return buffer;
  815. }
  816. return String.fromCharCode(buffer.readUInt8(0) + 97);
  817. }
  818. }), 'a=b');
  819. st.equal(qs.stringify({ a: SaferBuffer.from('a b') }, {
  820. encoder: function (buffer) {
  821. return buffer;
  822. }
  823. }), 'a=a b');
  824. st.end();
  825. });
  826. t.test('serializeDate option', function (st) {
  827. var date = new Date();
  828. st.equal(
  829. qs.stringify({ a: date }),
  830. 'a=' + date.toISOString().replace(/:/g, '%3A'),
  831. 'default is toISOString'
  832. );
  833. var mutatedDate = new Date();
  834. mutatedDate.toISOString = function () {
  835. throw new SyntaxError();
  836. };
  837. st['throws'](function () {
  838. mutatedDate.toISOString();
  839. }, SyntaxError);
  840. st.equal(
  841. qs.stringify({ a: mutatedDate }),
  842. 'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'),
  843. 'toISOString works even when method is not locally present'
  844. );
  845. var specificDate = new Date(6);
  846. st.equal(
  847. qs.stringify(
  848. { a: specificDate },
  849. { serializeDate: function (d) { return d.getTime() * 7; } }
  850. ),
  851. 'a=42',
  852. 'custom serializeDate function called'
  853. );
  854. st.equal(
  855. qs.stringify(
  856. { a: [date] },
  857. {
  858. serializeDate: function (d) { return d.getTime(); },
  859. arrayFormat: 'comma'
  860. }
  861. ),
  862. 'a=' + date.getTime(),
  863. 'works with arrayFormat comma'
  864. );
  865. st.equal(
  866. qs.stringify(
  867. { a: [date] },
  868. {
  869. serializeDate: function (d) { return d.getTime(); },
  870. arrayFormat: 'comma',
  871. commaRoundTrip: true
  872. }
  873. ),
  874. 'a%5B%5D=' + date.getTime(),
  875. 'works with arrayFormat comma'
  876. );
  877. st.end();
  878. });
  879. t.test('RFC 1738 serialization', function (st) {
  880. st.equal(qs.stringify({ a: 'b c' }, { format: qs.formats.RFC1738 }), 'a=b+c');
  881. st.equal(qs.stringify({ 'a b': 'c d' }, { format: qs.formats.RFC1738 }), 'a+b=c+d');
  882. st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }, { format: qs.formats.RFC1738 }), 'a+b=a+b');
  883. st.equal(qs.stringify({ 'foo(ref)': 'bar' }, { format: qs.formats.RFC1738 }), 'foo(ref)=bar');
  884. st.end();
  885. });
  886. t.test('RFC 3986 spaces serialization', function (st) {
  887. st.equal(qs.stringify({ a: 'b c' }, { format: qs.formats.RFC3986 }), 'a=b%20c');
  888. st.equal(qs.stringify({ 'a b': 'c d' }, { format: qs.formats.RFC3986 }), 'a%20b=c%20d');
  889. st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }, { format: qs.formats.RFC3986 }), 'a%20b=a%20b');
  890. st.end();
  891. });
  892. t.test('Backward compatibility to RFC 3986', function (st) {
  893. st.equal(qs.stringify({ a: 'b c' }), 'a=b%20c');
  894. st.equal(qs.stringify({ 'a b': SaferBuffer.from('a b') }), 'a%20b=a%20b');
  895. st.end();
  896. });
  897. t.test('Edge cases and unknown formats', function (st) {
  898. ['UFO1234', false, 1234, null, {}, []].forEach(function (format) {
  899. st['throws'](
  900. function () {
  901. qs.stringify({ a: 'b c' }, { format: format });
  902. },
  903. new TypeError('Unknown format option provided.')
  904. );
  905. });
  906. st.end();
  907. });
  908. t.test('encodeValuesOnly', function (st) {
  909. st.equal(
  910. qs.stringify(
  911. { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
  912. { encodeValuesOnly: true, arrayFormat: 'indices' }
  913. ),
  914. 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h',
  915. 'encodeValuesOnly + indices'
  916. );
  917. st.equal(
  918. qs.stringify(
  919. { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
  920. { encodeValuesOnly: true, arrayFormat: 'brackets' }
  921. ),
  922. 'a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h',
  923. 'encodeValuesOnly + brackets'
  924. );
  925. st.equal(
  926. qs.stringify(
  927. { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] },
  928. { encodeValuesOnly: true, arrayFormat: 'repeat' }
  929. ),
  930. 'a=b&c=d&c=e%3Df&f=g&f=h',
  931. 'encodeValuesOnly + repeat'
  932. );
  933. st.equal(
  934. qs.stringify(
  935. { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
  936. { arrayFormat: 'indices' }
  937. ),
  938. 'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h',
  939. 'no encodeValuesOnly + indices'
  940. );
  941. st.equal(
  942. qs.stringify(
  943. { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
  944. { arrayFormat: 'brackets' }
  945. ),
  946. 'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h',
  947. 'no encodeValuesOnly + brackets'
  948. );
  949. st.equal(
  950. qs.stringify(
  951. { a: 'b', c: ['d', 'e'], f: [['g'], ['h']] },
  952. { arrayFormat: 'repeat' }
  953. ),
  954. 'a=b&c=d&c=e&f=g&f=h',
  955. 'no encodeValuesOnly + repeat'
  956. );
  957. st.end();
  958. });
  959. t.test('encodeValuesOnly - strictNullHandling', function (st) {
  960. st.equal(
  961. qs.stringify(
  962. { a: { b: null } },
  963. { encodeValuesOnly: true, strictNullHandling: true }
  964. ),
  965. 'a[b]'
  966. );
  967. st.end();
  968. });
  969. t.test('throws if an invalid charset is specified', function (st) {
  970. st['throws'](function () {
  971. qs.stringify({ a: 'b' }, { charset: 'foobar' });
  972. }, new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'));
  973. st.end();
  974. });
  975. t.test('respects a charset of iso-8859-1', function (st) {
  976. st.equal(qs.stringify({ æ: 'æ' }, { charset: 'iso-8859-1' }), '%E6=%E6');
  977. st.end();
  978. });
  979. t.test('encodes unrepresentable chars as numeric entities in iso-8859-1 mode', function (st) {
  980. st.equal(qs.stringify({ a: '☺' }, { charset: 'iso-8859-1' }), 'a=%26%239786%3B');
  981. st.end();
  982. });
  983. t.test('respects an explicit charset of utf-8 (the default)', function (st) {
  984. st.equal(qs.stringify({ a: 'æ' }, { charset: 'utf-8' }), 'a=%C3%A6');
  985. st.end();
  986. });
  987. t.test('`charsetSentinel` option', function (st) {
  988. st.equal(
  989. qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' }),
  990. 'utf8=%E2%9C%93&a=%C3%A6',
  991. 'adds the right sentinel when instructed to and the charset is utf-8'
  992. );
  993. st.equal(
  994. qs.stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' }),
  995. 'utf8=%26%2310003%3B&a=%E6',
  996. 'adds the right sentinel when instructed to and the charset is iso-8859-1'
  997. );
  998. st.end();
  999. });
  1000. t.test('does not mutate the options argument', function (st) {
  1001. var options = {};
  1002. qs.stringify({}, options);
  1003. st.deepEqual(options, {});
  1004. st.end();
  1005. });
  1006. t.test('strictNullHandling works with custom filter', function (st) {
  1007. var filter = function (prefix, value) {
  1008. return value;
  1009. };
  1010. var options = { strictNullHandling: true, filter: filter };
  1011. st.equal(qs.stringify({ key: null }, options), 'key');
  1012. st.end();
  1013. });
  1014. t.test('strictNullHandling works with null serializeDate', function (st) {
  1015. var serializeDate = function () {
  1016. return null;
  1017. };
  1018. var options = { strictNullHandling: true, serializeDate: serializeDate };
  1019. var date = new Date();
  1020. st.equal(qs.stringify({ key: date }, options), 'key');
  1021. st.end();
  1022. });
  1023. t.test('allows for encoding keys and values differently', function (st) {
  1024. var encoder = function (str, defaultEncoder, charset, type) {
  1025. if (type === 'key') {
  1026. return defaultEncoder(str, defaultEncoder, charset, type).toLowerCase();
  1027. }
  1028. if (type === 'value') {
  1029. return defaultEncoder(str, defaultEncoder, charset, type).toUpperCase();
  1030. }
  1031. throw 'this should never happen! type: ' + type;
  1032. };
  1033. st.deepEqual(qs.stringify({ KeY: 'vAlUe' }, { encoder: encoder }), 'key=VALUE');
  1034. st.end();
  1035. });
  1036. t.test('objects inside arrays', function (st) {
  1037. var obj = { a: { b: { c: 'd', e: 'f' } } };
  1038. var withArray = { a: { b: [{ c: 'd', e: 'f' }] } };
  1039. st.equal(qs.stringify(obj, { encode: false }), 'a[b][c]=d&a[b][e]=f', 'no array, no arrayFormat');
  1040. st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'brackets' }), 'a[b][c]=d&a[b][e]=f', 'no array, bracket');
  1041. st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'indices' }), 'a[b][c]=d&a[b][e]=f', 'no array, indices');
  1042. st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'repeat' }), 'a[b][c]=d&a[b][e]=f', 'no array, repeat');
  1043. st.equal(qs.stringify(obj, { encode: false, arrayFormat: 'comma' }), 'a[b][c]=d&a[b][e]=f', 'no array, comma');
  1044. st.equal(qs.stringify(withArray, { encode: false }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, no arrayFormat');
  1045. st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'brackets' }), 'a[b][][c]=d&a[b][][e]=f', 'array, bracket');
  1046. st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'indices' }), 'a[b][0][c]=d&a[b][0][e]=f', 'array, indices');
  1047. st.equal(qs.stringify(withArray, { encode: false, arrayFormat: 'repeat' }), 'a[b][c]=d&a[b][e]=f', 'array, repeat');
  1048. st.equal(
  1049. qs.stringify(withArray, { encode: false, arrayFormat: 'comma' }),
  1050. '???',
  1051. 'array, comma',
  1052. { skip: 'TODO: figure out what this should do' }
  1053. );
  1054. st.end();
  1055. });
  1056. t.test('stringifies sparse arrays', function (st) {
  1057. /* eslint no-sparse-arrays: 0 */
  1058. st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1]=2&a[4]=1');
  1059. st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[]=2&a[]=1');
  1060. st.equal(qs.stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a=2&a=1');
  1061. st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][b][2][c]=1');
  1062. st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][b][][c]=1');
  1063. st.equal(qs.stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[b][c]=1');
  1064. st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][2][3][c]=1');
  1065. st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][][][c]=1');
  1066. st.equal(qs.stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[c]=1');
  1067. st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), 'a[1][2][3][c][1]=1');
  1068. st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), 'a[][][][c][]=1');
  1069. st.equal(qs.stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), 'a[c]=1');
  1070. st.end();
  1071. });
  1072. t.test('encodes a very long string', function (st) {
  1073. var chars = [];
  1074. var expected = [];
  1075. for (var i = 0; i < 5e3; i++) {
  1076. chars.push(' ' + i);
  1077. expected.push('%20' + i);
  1078. }
  1079. var obj = {
  1080. foo: chars.join('')
  1081. };
  1082. st.equal(
  1083. qs.stringify(obj, { arrayFormat: 'brackets', charset: 'utf-8' }),
  1084. 'foo=' + expected.join('')
  1085. );
  1086. st.end();
  1087. });
  1088. t.end();
  1089. });
  1090. test('stringifies empty keys', function (t) {
  1091. emptyTestCases.forEach(function (testCase) {
  1092. t.test('stringifies an object with empty string key with ' + testCase.input, function (st) {
  1093. st.deepEqual(
  1094. qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'indices' }),
  1095. testCase.stringifyOutput.indices,
  1096. 'test case: ' + testCase.input + ', indices'
  1097. );
  1098. st.deepEqual(
  1099. qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'brackets' }),
  1100. testCase.stringifyOutput.brackets,
  1101. 'test case: ' + testCase.input + ', brackets'
  1102. );
  1103. st.deepEqual(
  1104. qs.stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'repeat' }),
  1105. testCase.stringifyOutput.repeat,
  1106. 'test case: ' + testCase.input + ', repeat'
  1107. );
  1108. st.end();
  1109. });
  1110. });
  1111. t.test('edge case with object/arrays', function (st) {
  1112. st.deepEqual(qs.stringify({ '': { '': [2, 3] } }, { encode: false }), '[][0]=2&[][1]=3');
  1113. st.deepEqual(qs.stringify({ '': { '': [2, 3], a: 2 } }, { encode: false }), '[][0]=2&[][1]=3&[a]=2');
  1114. st.deepEqual(qs.stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' }), '[][0]=2&[][1]=3');
  1115. st.deepEqual(qs.stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' }), '[][0]=2&[][1]=3&[a]=2');
  1116. st.end();
  1117. });
  1118. t.test('stringifies non-string keys', function (st) {
  1119. var actual = qs.stringify({ a: 'b', 'false': {} }, {
  1120. filter: ['a', false, null],
  1121. allowDots: true,
  1122. encodeDotInKeys: true
  1123. });
  1124. st.equal(actual, 'a=b', 'stringifies correctly');
  1125. st.end();
  1126. });
  1127. });