@@ -128,6 +128,30 @@ describe('parseChunked()', () => {
128128 } ) ;
129129
130130 describe ( 'errors' , ( ) => {
131+ it ( 'unmatched closing bracket at start' , ( ) =>
132+ assert . rejects (
133+ ( ) => parseChunked ( [ ']' ] ) ,
134+ / U n e x p e c t e d t o k e n ] i n J S O N a t p o s i t i o n 0 | U n e x p e c t e d t o k e n ' ] ' ( , " ] " i s n o t v a l i d J S O N ) ? /
135+ )
136+ ) ;
137+ it ( 'unmatched closing brace at start' , ( ) =>
138+ assert . rejects (
139+ ( ) => parseChunked ( [ '}' ] ) ,
140+ / U n e x p e c t e d t o k e n } i n J S O N a t p o s i t i o n 0 | U n e x p e c t e d t o k e n ' } ' ( , " } " i s n o t v a l i d J S O N ) ? /
141+ )
142+ ) ;
143+ it ( 'extra token after complete value' , ( ) =>
144+ assert . rejects (
145+ ( ) => parseChunked ( [ '[] true' ] ) ,
146+ / ( U n e x p e c t e d t o k e n t i n J S O N a t p o s i t i o n 3 | U n e x p e c t e d t o k e n t i n J S O N a t p o s i t i o n 6 | U n e x p e c t e d n o n - w h i t e s p a c e c h a r a c t e r a f t e r J S O N a t p o s i t i o n 2 | E x p e c t e d ' , ' o r ' ] ' a f t e r a r r a y e l e m e n t i n J S O N a t p o s i t i o n 3 ) /
147+ )
148+ ) ;
149+ it ( 'extra opening after root' , ( ) =>
150+ assert . rejects (
151+ ( ) => parseChunked ( [ '{}[' ] ) ,
152+ / ( U n e x p e c t e d t o k e n \[ i n J S O N a t p o s i t i o n 2 | U n e x p e c t e d n o n - w h i t e s p a c e c h a r a c t e r a f t e r J S O N a t p o s i t i o n 2 ) /
153+ )
154+ ) ;
131155 it ( 'abs pos across chunks' , ( ) =>
132156 assert . rejects (
133157 async ( ) => await parse ( [ '{"test":"he' , 'llo",}' ] ) ,
@@ -172,6 +196,49 @@ describe('parseChunked()', () => {
172196 ) ;
173197 } ) ;
174198
199+ describe ( 'trailing whitespace after full value' , ( ) => {
200+ it ( 'spaces and newlines after array' , async ( ) => {
201+ const actual = await parse ( [ '[1,2]\n\n \t ' ] ) ;
202+ assert . deepStrictEqual ( actual , [ 1 , 2 ] ) ;
203+ } ) ;
204+ it ( 'split chunks with trailing whitespace' , async ( ) => {
205+ const actual = await parse ( [ '[1,2]' , ' ' , '\n\t' ] ) ;
206+ assert . deepStrictEqual ( actual , [ 1 , 2 ] ) ;
207+ } ) ;
208+ } ) ;
209+
210+ describe ( 'chunk boundary for escapes and multi-byte utf-8' , ( ) => {
211+ it ( 'escaped quote split' , async ( ) => {
212+ const actual = await parse ( [ '"hello \\"' , 'world"' ] ) ;
213+ assert . deepStrictEqual ( actual , 'hello "world' ) ;
214+ } ) ;
215+ it ( 'backslash escape split across chunks' , async ( ) => {
216+ // create a string with a literal backslash then a quote and more text: "foo \"bar"
217+ const chunks = [ '"foo \\"' , 'bar"' ] ;
218+ const actual = await parse ( chunks ) ;
219+ assert . deepStrictEqual ( actual , 'foo "bar' ) ;
220+ } ) ;
221+ it ( 'multi-byte emoji split across chunks' , async ( ) => {
222+ const json = JSON . stringify ( 'a😅b' ) ;
223+ // split inside surrogate pair intentionally
224+ const first = json . slice ( 0 , 4 ) ; // "a
225+ const middle = json . slice ( 4 , 6 ) ; // first part of surrogate maybe
226+ const rest = json . slice ( 6 ) ;
227+ const actual = await parse ( [ first , middle , rest ] ) ;
228+ assert . deepStrictEqual ( actual , 'a😅b' ) ;
229+ } ) ;
230+ it ( 'multi-byte via Uint8Array boundary' , async ( ) => {
231+ const str = '"start 🤓 end"' ;
232+ const enc = new TextEncoder ( ) . encode ( str ) ;
233+ // slice across multi-byte boundary of 🤓 (U+1F913)
234+ const idx = enc . indexOf ( 0xF0 ) ; // start of 4-byte sequence
235+ const part1 = enc . slice ( 0 , idx + 2 ) ; // cut in middle of sequence
236+ const part2 = enc . slice ( idx + 2 ) ;
237+ const actual = await parseChunked ( [ part1 , part2 ] ) ;
238+ assert . deepStrictEqual ( actual , 'start 🤓 end' ) ;
239+ } ) ;
240+ } ) ;
241+
175242 describe ( 'use with buffers' , ( ) => {
176243 const input = '[1234,{"🤓\\uD800\\uDC00":"🤓\\uD800\\uDC00\\u006f\\ufffd\\uffff\\ufffd"}]' ;
177244 const expected = [ 1234 , { '🤓\uD800\uDC00' : '🤓\uD800\uDC00\u006f\ufffd\uffff\ufffd' } ] ;
0 commit comments