Repository: dai-shi/waku
Pull request #855 (2024-08-29 00:51:54)

feat(website): refine sidebar

vercel[bot]: [vc]: #tpDk6omoh+88j1oBs02KYqLRz+r/TnH/R1i6I+Hk2Cs=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ3YWt1Iiwicm9vdERpcmVjdG9yeSI6bnVsbCwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL2RhaXNoaXMtcHJvamVjdHMvd2FrdS9SR0RqV3llMzdNYmkzSEVUdjVUUUVrcmVNeW9pIiwicHJldmlld1VybCI6Indha3UtZ2l0LWZvcmstY25keWNkLXdlYnNpdGUtZGFpc2hpcy1wcm9qZWN0cy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IlBFTkRJTkcifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **waku** | 🔄 Building ([Inspect](https://vercel.com/daishis-projects/waku/RGDjWye37Mbi3HETv5TQEkreMyoi)) | [Visit Preview](https://waku-git-fork-cndycd-website-daishis-projects.vercel.app) | Aug 29, 2024 0:51am |


codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/dai-shi/waku/pr/855/builds/536372) or the icon next to each commit SHA.

Patch:
diff --git a/packages/waku/src/lib/renderers/rsc-renderer.ts b/packages/waku/src/lib/renderers/rsc-renderer.ts
index 496d3db9..d5ffea1a 100644
--- a/packages/waku/src/lib/renderers/rsc-renderer.ts
+++ b/packages/waku/src/lib/renderers/rsc-renderer.ts
@@ -9,9 +9,9 @@ import type {
 } from '../../server.js';
 import type { ResolvedConfig } from '../config.js';
 import { filePathToFileURL } from '../utils/path.js';
-import { parseFormData } from '../utils/form.js';
-import { streamToString } from '../utils/stream.js';
+import { streamToArrayBuffer } from '../utils/stream.js';
 import { decodeActionId } from '../renderers/utils.js';
+import { bufferToString, parseFormData } from '../utils/buffer.js';
 
 export const SERVER_MODULE_MAP = {
   'rsdw-server': 'react-server-dom-webpack/server.edge',
@@ -194,15 +194,16 @@ export async function renderRsc(
 
   let decodedBody: unknown | undefined = args.decodedBody;
   if (body) {
-    const bodyStr = await streamToString(body);
+    const bodyBuf = await streamToArrayBuffer(body);
     if (
       typeof contentType === 'string' &&
       contentType.startsWith('multipart/form-data')
     ) {
       // XXX This doesn't support streaming unlike busboy
-      const formData = parseFormData(bodyStr, contentType);
+      const formData = await parseFormData(bodyBuf, contentType);
       decodedBody = await decodeReply(formData, serverBundlerConfig);
-    } else if (bodyStr) {
+    } else if (bodyBuf.byteLength > 0) {
+      const bodyStr = bufferToString(bodyBuf);
       decodedBody = await decodeReply(bodyStr, serverBundlerConfig);
     }
   }
diff --git a/packages/waku/src/lib/utils/buffer.ts b/packages/waku/src/lib/utils/buffer.ts
new file mode 100644
index 00000000..7cd9c5ce
--- /dev/null
+++ b/packages/waku/src/lib/utils/buffer.ts
@@ -0,0 +1,16 @@
+export const parseFormData = async (
+  buffer: ArrayBuffer,
+  contentType: string,
+): Promise<FormData> => {
+  const response = new Response(buffer, {
+    headers: {
+      'content-type': contentType,
+    },
+  });
+  return response.formData();
+};
+
+export const bufferToString = (buffer: ArrayBuffer): string => {
+  const enc = new TextDecoder();
+  return enc.decode(buffer);
+};
diff --git a/packages/waku/src/lib/utils/form.ts b/packages/waku/src/lib/utils/form.ts
deleted file mode 100644
index a33c9214..00000000
--- a/packages/waku/src/lib/utils/form.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-// TODO is this correct? better to use a library?
-export const parseFormData = (body: string, contentType: string) => {
-  const boundary = contentType.split('boundary=')[1];
-  const parts = body.split(`--${boundary}`);
-  const formData = new FormData();
-  for (const part of parts) {
-    if (part.trim() === '' || part === '--') continue;
-    const [rawHeaders, content] = part.split('\r\n\r\n', 2);
-    const headers = rawHeaders!.split('\r\n').reduce(
-      (acc, currentHeader) => {
-        const [key, value] = currentHeader.split(': ');
-        acc[key!.toLowerCase()] = value!;
-        return acc;
-      },
-      {} as Record<string, string>,
-    );
-    const contentDisposition = headers['content-disposition'];
-    const nameMatch = /name="([^"]+)"/.exec(contentDisposition!);
-    const filenameMatch = /filename="([^"]+)"/.exec(contentDisposition!);
-    if (nameMatch) {
-      const name = nameMatch[1];
-      if (filenameMatch) {
-        const filename = filenameMatch[1];
-        const type = headers['content-type'] || 'application/octet-stream';
-        const blob = new Blob([content!], { type });
-        formData.append(name!, blob, filename);
-      } else {
-        formData.append(name!, content!.trim());
-      }
-    }
-  }
-  return formData;
-};
diff --git a/packages/waku/src/lib/utils/stream.ts b/packages/waku/src/lib/utils/stream.ts
index d70ccc5a..5dfa4f15 100644
--- a/packages/waku/src/lib/utils/stream.ts
+++ b/packages/waku/src/lib/utils/stream.ts
@@ -11,6 +11,32 @@ export const concatUint8Arrays = (arrs: Uint8Array[]): Uint8Array => {
   return array;
 };
 
+// FIXME remove the two loops if possible, if it is more efficient
+export const streamToArrayBuffer = async (stream: ReadableStream) => {
+  const reader = stream.getReader();
+  const chunks = [];
+  let totalSize = 0;
+  let done = false;
+  let value: Uint8Array | undefined;
+
+  do {
+    ({ done, value } = await reader.read());
+    if (!done && value) {
+      chunks.push(value);
+      totalSize += value.length;
+    }
+  } while (!done);
+
+  const result = new Uint8Array(totalSize);
+  let offset = 0;
+  for (const chunk of chunks) {
+    result.set(chunk, offset);
+    offset += chunk.length;
+  }
+
+  return result.buffer;
+};
+
 export const streamToString = async (
   stream: ReadableStream,
 ): Promise<string> => {
diff --git a/packages/website/src/components/navigation.tsx b/packages/website/src/components/navigation.tsx
index be2994cc..5cdf69f6 100644
--- a/packages/website/src/components/navigation.tsx
+++ b/packages/website/src/components/navigation.tsx
@@ -31,7 +31,7 @@ export const Navigation = ({ isHome }: NavigationProps) => {
         'fixed inset-0 z-90 flex-shrink-0 flex-col overflow-clip border-gray-800 bg-gray-950 transition-opacity duration-300 ease-in-out xl:pointer-events-auto xl:bottom-auto xl:left-0 xl:right-auto xl:top-0 xl:z-[9999] xl:h-screen xl:border-r 2xl:left-12 2xl:top-12 2xl:h-[calc(100vh-6rem)] 2xl:rounded-xl 2xl:border',
       )}
     >
-      <div className="relative z-10 flex h-full max-h-full w-full flex-shrink-0 flex-col items-center justify-start gap-8 !overflow-y-auto p-8 text-white md:p-8 xl:gap-12 xl:p-12">
+      <div className="relative z-10 flex h-full max-h-full w-full flex-shrink-0 flex-col items-center justify-start gap-8 !overflow-y-auto p-8 pb-16 text-white md:justify-center md:p-8 xl:justify-start xl:gap-12 xl:p-12">
         <div className="flex w-full flex-col items-center justify-center">
           {isHome ? (
             <button
@@ -59,7 +59,7 @@ export const Navigation = ({ isHome }: NavigationProps) => {
             The minimal React framework
           </div>
         </div>
-        <div className="relative flex max-h-full w-full max-w-[16rem] flex-grow flex-col gap-0.5 md:left-auto xl:gap-1.5">
+        <div className="relative flex max-h-full w-full max-w-[16rem] flex-col gap-0.5 md:left-auto xl:flex-grow xl:gap-1.5">
           {docs.map((link) => {
             return isHome ? (
               <DocLink key={link.to} link={link} />
@@ -72,7 +72,7 @@ export const Navigation = ({ isHome }: NavigationProps) => {
           {links.map((link) => {
             return <MenuLink key={link.to} link={link} />;
           })}
-          <Sponsors />
+          <Sponsors className="pt-4" />
         </ul>
       </div>
     </nav>
diff --git a/packages/website/src/components/sponsors.tsx b/packages/website/src/components/sponsors.tsx
index 18142431..348abddf 100644
--- a/packages/website/src/components/sponsors.tsx
+++ b/packages/website/src/components/sponsors.tsx
@@ -1,27 +1,30 @@
 'use client';
 
+import { useState } from 'react';
 import cx from 'classnames';
 
+import { useInterval } from '../hooks';
+
 type SponsorsProps = {
   className?: string;
 };
 
+const INTERVAL = 5_000;
+
 export const Sponsors = ({ className = '' }: SponsorsProps) => {
+  const [activeIndex, setActiveIndex] = useState<number>(0);
+
+  useInterval(() => {
+    setActiveIndex((prevIndex) => (prevIndex + 1) % sponsors.length);
+  }, INTERVAL);
+
   if (import.meta.env.WAKU_PUBLIC_SHOW_SPONSORS !== 'YES') return null;
 
   return (
-    <div
-      className={cx(
-        'pointer-events-none transition-opacity duration-500 ease-in-out',
-        className,
-      )}
-    >
-      <a
-        href="https://vercel.com/home"
-        target="_blank"
-        rel="noopener noreferrer"
+    <div className={cx('pointer-events-none relative w-full', className)}>
+      <div
         className={cx(
-          'group pointer-events-auto inline-flex w-full origin-center scale-75 flex-col items-center gap-[4px] sm:scale-100',
+          'group pointer-events-auto flex w-full origin-center scale-75 flex-col items-center gap-[4px] sm:scale-100',
         )}
       >
         <span
@@ -31,12 +34,46 @@ export const Sponsors = ({ className = '' }: SponsorsProps) => {
         >
           sponsored by
         </span>
-        <img
-          src="https://cdn.candycode.com/waku/vercel.svg"
-          alt="Vercel"
-          className="h-[20px] w-auto object-contain"
-        />
-      </a>
+        <div className="relative w-full">
+          {sponsors.map((sponsors, index) => {
+            return (
+              <a
+                key={index}
+                href={sponsors.url}
+                target="_blank"
+                rel="noopener noreferrer"
+                className={cx(
+                  'transition-opacity duration-1000 ease-in-out',
+                  index !== activeIndex
+                    ? 'pointer-events-none opacity-0'
+                    : 'delay-500',
+                  index !== 0 && 'absolute inset-0 h-full w-full',
+                )}
+              >
+                <div className="absolute left-0 right-0 top-0 h-[20px] -translate-y-full" />
+                <img
+                  src={sponsors.logo}
+                  alt={sponsors.title}
+                  className="block h-[20px] w-full object-contain"
+                />
+              </a>
+            );
+          })}
+        </div>
+      </div>
     </div>
   );
 };
+
+const sponsors = [
+  {
+    title: 'Vercel',
+    logo: 'https://cdn.candycode.com/waku/vercel.svg',
+    url: 'https://vercel.com/home',
+  },
+  {
+    title: 'Progress KendoReact',
+    logo: 'https://cdn.candycode.com/waku/sponsors/kendo-react.png',
+    url: 'https://www.telerik.com/kendo-react-ui',
+  },
+];
diff --git a/packages/website/src/hooks/index.ts b/packages/website/src/hooks/index.ts
index 4470b736..0a6e658d 100644
--- a/packages/website/src/hooks/index.ts
+++ b/packages/website/src/hooks/index.ts
@@ -1,6 +1,6 @@
 'use client';
 
-import { useEffect, useCallback } from 'react';
+import { useEffect, useCallback, useRef } from 'react';
 
 export const useOnEscape = (handler: () => void) => {
   const handleEscape = useCallback(
@@ -43,3 +43,24 @@ export const useOnClickOutside = (
     };
   }, [handler, node]);
 };
+
+export const useInterval = (callback: () => void, delay: number) => {
+  const intervalRef = useRef<any>(null);
+
+  const savedCallback = useRef(callback);
+
+  useEffect(() => {
+    savedCallback.current = callback;
+  }, [callback]);
+
+  useEffect(() => {
+    const tick = () => savedCallback.current();
+
+    if (typeof delay === 'number') {
+      intervalRef.current = window.setInterval(tick, delay);
+      return () => window.clearInterval(intervalRef.current);
+    }
+  }, [delay]);
+
+  return intervalRef;
+};


Test patch:
diff --git a/packages/waku/tests/buffer.test.ts b/packages/waku/tests/buffer.test.ts
new file mode 100644
index 00000000..b99c8d28
--- /dev/null
+++ b/packages/waku/tests/buffer.test.ts
@@ -0,0 +1,338 @@
+import { describe, it, expect } from 'vitest';
+import { bufferToString, parseFormData } from '../src/lib/utils/buffer';
+
+// Minimal valid 1x1 pixel PNG image
+const PNG_HEADER = new Uint8Array([
+  0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00, 0x00, 0x0d, 0x49,
+  0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06,
+  0x00, 0x00, 0x00, 0x1f, 0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44,
+  0x41, 0x54, 0x08, 0xd7, 0x63, 0x60, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01, 0xe2,
+  0x21, 0xbc, 0x33, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4e, 0x44, 0xae, 0x42,
+  0x60, 0x82,
+]);
+
+describe('parseFormData', () => {
+  it('should parse text fields correctly', async () => {
+    const boundary = 'boundary123';
+    const body = `--${boundary}\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--${boundary}--`;
+    const contentType = `multipart/form-data; boundary=${boundary}`;
+
+    const formData = await parseFormData(
+      new TextEncoder().encode(body),
+      contentType,
+    );
+
+    expect(formData.get('field1')).toBe('value1');
+  });
+
+  it('should parse multiple text fields', async () => {
+    const boundary = 'boundary123';
+    const body = `--${boundary}\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--${boundary}\r\nContent-Disposition: form-data; name="field2"\r\n\r\nvalue2\r\n--${boundary}--`;
+    const contentType = `multipart/form-data; boundary=${boundary}`;
+
+    const formData = await parseFormData(
+      new TextEncoder().encode(body),
+      contentType,
+    );
+
+    expect(formData.get('field1')).toBe('value1');
+    expect(formData.get('field2')).toBe('value2');
+  });
+
+  it('should parse file fields correctly', async () => {
+    const boundary = 'boundary123';
+    const body = `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\nfile content\r\n--${boundary}--`;
+    const contentType = `multipart/form-data; boundary=${boundary}`;
+
+    const formData = await parseFormData(
+      new TextEncoder().encode(body),
+      contentType,
+    );
+
+    const file = formData.get('file') as File;
+    expect(file.name).toBe('test.txt');
+    expect(file.type).toBe('text/plain');
+    expect(file.size).toBe(12);
+    expect(await file.text()).toBe('file content');
+  });
+
+  it('should handle mixed text and file fields', async () => {
+    const boundary = 'boundary123';
+    const body = `--${boundary}\r\nContent-Disposition: form-data; name="field1"\r\n\r\nvalue1\r\n--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="test.txt"\r\nContent-Type: text/plain\r\n\r\nfile content\r\n--${boundary}--`;
+    const contentType = `multipart/form-data; boundary=${boundary}`;
+
+    const formData = await parseFormData(
+      new TextEncoder().encode(body),
+      contentType,
+    );
+
+    expect(formData.get('field1')).toBe('value1');
+    const file = formData.get('file') as File;
+    expect(file.name).toBe('test.txt');
+    expect(file.type).toBe('text/plain');
+    expect(file.size).toBe(12);
+    expect(await file.text()).toBe('file content');
+  });
+
+  it('should handle empty fields', async () => {
+    const boundary = 'boundary123';
+    const body = `--${boundary}\r\nContent-Disposition: form-data; name="emptyField"\r\n\r\n\r\n--${boundary}--`;
+    const contentType = `multipart/form-data; boundary=${boundary}`;
+
+    const formData = await parseFormData(
+      new TextEncoder().encode(body),
+      contentType,
+    );
+
+    expect(formData.get('emptyField')).toBe('');
+  });
+
+  it('should handle fields with special characters', async () => {
+    const boundary = 'boundary123';
+    const body = `--${boundary}\r\nContent-Disposition: form-data; name="special"\r\n\r\n!@#$%^&*()\r\n--${boundary}--`;
+    const contentType = `multipart/form-data; boundary=${boundary}`;
+
+    const formData = await parseFormData(
+      new TextEncoder().encode(body),
+      contentType,
+    );
+
+    expect(formData.get('special')).toBe('!@#$%^&*()');
+  });
+
+  it('should handle fields with line breaks', async () => {
+    const boundary = 'boundary123';
+    const body = `--${boundary}\r\nContent-Disposition: form-data; name="multiline"\r\n\r\nLine 1\r\nLine 2\r\nLine 3\r\n--${boundary}--`;
+    const contentType = `multipart/form-data; boundary=${boundary}`;
+
+    const formData = await parseFormData(
+      new TextEncoder().encode(body),
+      contentType,
+    );
+
+    expect(formData.get('multiline')).toBe('Line 1\r\nLine 2\r\nLine 3');
+  });
+
+  it('should parse PNG file fields correctly and match input', async () => {
+    const formData = new FormData();
+    formData.append(
+      'pngFile',
+      new Blob([PNG_HEADER], { type: 'image/png' }),
+      'test.png',
+    );
+
+    // Create a Request object
+    const request = new Request('http://example.com/upload', {
+      method: 'POST',
+      body: formData,
+    });
+
+    const contentType = request.headers.get('Content-Type');
+
+    // Get the body as ArrayBuffer
+    const arrayBuffer = await request.arrayBuffer();
+
+    // Convert ArrayBuffer to Uint8Array
+    const body = new Uint8Array(arrayBuffer);
+
+    // Now use your parseFormData function
+    const parsedFormData = await parseFormData(body, contentType!);
+
+    const file = parsedFormData.get('pngFile') as File;
+    expect(file.name).toBe('test.png');
+    expect(file.type).toBe('image/png');
+    expect(file.size).toBe(PNG_HEADER.length);
+
+    const fileArrayBuffer = await file.arrayBuffer();
+    const fileUint8Array = new Uint8Array(fileArrayBuffer);
+    expect(fileUint8Array).toEqual(PNG_HEADER);
+  });
+
+  it('should handle mixed text, text file, and PNG file fields and match input', async () => {
+    const formData = new FormData();
+    formData.append('textField', 'Hello, World!');
+    formData.append(
+      'textFile',
+      new Blob(['This is a text file.'], { type: 'text/plain' }),
+      'test.txt',
+    );
+    formData.append(
+      'pngFile',
+      new Blob([PNG_HEADER], { type: 'image/png' }),
+      'test.png',
+    );
+
+    // Create a Request object
+    const request = new Request('http://example.com/upload', {
+      method: 'POST',
+      body: formData,
+    });
+
+    const contentType = request.headers.get('Content-Type');
+
+    // Get the body as ArrayBuffer
+    const arrayBuffer = await request.arrayBuffer();
+
+    // Convert ArrayBuffer to Uint8Array
+    const body = new Uint8Array(arrayBuffer);
+
+    // Now use your parseFormData function
+    const parsedFormData = await parseFormData(body, contentType!);
+
+    expect(parsedFormData.get('textField')).toBe('Hello, World!');
+
+    const textFile = parsedFormData.get('textFile') as File;
+    expect(textFile.name).toBe('test.txt');
+    expect(textFile.type).toBe('text/plain');
+    const textFileText = await textFile.text();
+    expect(textFileText).toBe('This is a text file.');
+
+    const pngFile = parsedFormData.get('pngFile') as File;
+    expect(pngFile.name).toBe('test.png');
+    expect(pngFile.type).toBe('image/png');
+    expect(pngFile.size).toBe(PNG_HEADER.length);
+    const pngFileArrayBuffer = await pngFile.arrayBuffer();
+    const pngFileUint8Array = new Uint8Array(pngFileArrayBuffer);
+    expect(pngFileUint8Array).toEqual(PNG_HEADER);
+  });
+
+  it('should handle base64 encoded content', async () => {
+    const boundary = 'boundary123';
+    const originalContent = 'Hello, World!';
+    const base64Content = Buffer.from(originalContent).toString('base64');
+    const body = `--${boundary}\r\nContent-Disposition: form-data; name="base64Field"\r\nContent-Transfer-Encoding: base64\r\n\r\n${base64Content}\r\n--${boundary}--`;
+    const contentType = `multipart/form-data; boundary=${boundary}`;
+
+    const formData = await parseFormData(
+      new TextEncoder().encode(body),
+      contentType,
+    );
+
+    const decodedContent = Buffer.from(
+      formData.get('base64Field') as string,
+      'base64',
+    ).toString();
+    expect(decodedContent).toBe(originalContent);
+  });
+
+  it('should handle binary content', async () => {
+    const formData = new FormData();
+    formData.append(
+      'binaryField',
+      new Blob([PNG_HEADER], { type: 'application/octet-stream' }),
+      'test.bin',
+    );
+
+    // Create a Request object
+    const request = new Request('http://example.com/upload', {
+      method: 'POST',
+      body: formData,
+    });
+
+    const contentType = request.headers.get('Content-Type');
+
+    // Get the body as ArrayBuffer
+    const arrayBuffer = await request.arrayBuffer();
+
+    // Convert ArrayBuffer to Uint8Array
+    const body = new Uint8Array(arrayBuffer);
+
+    // Now use your parseFormData function
+    const parsedFormData = await parseFormData(body, contentType!);
+
+    const file = parsedFormData.get('binaryField') as File;
+    expect(file.name).toBe('test.bin');
+    expect(file.type).toBe('application/octet-stream');
+    expect(file.size).toBe(PNG_HEADER.length);
+
+    const fileArrayBuffer = await file.arrayBuffer();
+    const fileUint8Array = new Uint8Array(fileArrayBuffer);
+    expect(fileUint8Array).toEqual(PNG_HEADER);
+  });
+
+  it('should handle mixed encodings in a single request', async () => {
+    const plainContent = 'Plain text';
+    const base64Content = Buffer.from('Base64 encoded text').toString('base64');
+    const binaryContent = new Uint8Array(PNG_HEADER);
+
+    const formData = new FormData();
+    formData.append('plainField', plainContent);
+    formData.append('base64Field', base64Content);
+    formData.append(
+      'binaryField',
+      new Blob([binaryContent], { type: 'application/octet-stream' }),
+      'test.bin',
+    );
+
+    // Create a Request object
+    const request = new Request('http://example.com/upload', {
+      method: 'POST',
+      body: formData,
+    });
+
+    const contentType = request.headers.get('Content-Type');
+
+    // Get the body as ArrayBuffer
+    const arrayBuffer = await request.arrayBuffer();
+
+    // Convert ArrayBuffer to Uint8Array
+    const body = new Uint8Array(arrayBuffer);
+
+    // Now use your parseFormData function
+    const parsedFormData = await parseFormData(body, contentType!);
+
+    expect(parsedFormData.get('plainField')).toBe(plainContent);
+    expect(
+      Buffer.from(
+        parsedFormData.get('base64Field') as string,
+        'base64',
+      ).toString(),
+    ).toBe('Base64 encoded text');
+
+    const file = parsedFormData.get('binaryField') as File;
+    expect(file.name).toBe('test.bin');
+    expect(file.type).toBe('application/octet-stream');
+    expect(file.size).toBe(PNG_HEADER.length);
+
+    const fileArrayBuffer = await file.arrayBuffer();
+    const fileUint8Array = new Uint8Array(fileArrayBuffer);
+    expect(fileUint8Array).toEqual(PNG_HEADER);
+  });
+});
+
+describe('bufferToString', () => {
+  it('should convert an empty ArrayBuffer to an empty string', () => {
+    const buffer = new ArrayBuffer(0);
+    expect(bufferToString(buffer)).toBe('');
+  });
+
+  it('should convert a simple ASCII string', () => {
+    const text = 'Hello, World!';
+    const buffer = new TextEncoder().encode(text).buffer;
+    expect(bufferToString(buffer)).toBe(text);
+  });
+
+  it('should handle Unicode characters', () => {
+    const text = '你好，世界！😊';
+    const buffer = new TextEncoder().encode(text).buffer;
+    expect(bufferToString(buffer)).toBe(text);
+  });
+
+  it('should handle a mix of ASCII and Unicode characters', () => {
+    const text = 'Hello, 世界! 🌍';
+    const buffer = new TextEncoder().encode(text).buffer;
+    expect(bufferToString(buffer)).toBe(text);
+  });
+
+  it('should handle a large string', () => {
+    const text = 'a'.repeat(1000000); // 1 million 'a' characters
+    const buffer = new TextEncoder().encode(text).buffer;
+    expect(bufferToString(buffer)).toBe(text);
+  });
+
+  it('should handle null characters', () => {
+    const text = 'Hello\0World';
+    const buffer = new TextEncoder().encode(text).buffer;
+    expect(bufferToString(buffer)).toBe(text);
+  });
+});