From edac11e6667e888f99b217a3ab3900809b56547c Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 29 Apr 2026 21:42:45 -0700 Subject: [PATCH 1/7] bump deps --- .bumpy/update-deps.md | 5 ++ bun.lock | 12 ++-- package.json | 6 +- src/lib/GoogleSpreadsheet.ts | 22 ++++--- src/test/exports.test.ts | 107 +++++++++++++++++++++++++++++++++++ 5 files changed, 135 insertions(+), 17 deletions(-) create mode 100644 .bumpy/update-deps.md create mode 100644 src/test/exports.test.ts diff --git a/.bumpy/update-deps.md b/.bumpy/update-deps.md new file mode 100644 index 0000000..33ca250 --- /dev/null +++ b/.bumpy/update-deps.md @@ -0,0 +1,5 @@ +--- +google-spreadsheet: patch +--- + +bump deps diff --git a/bun.lock b/bun.lock index e4d9e15..ddad1b5 100644 --- a/bun.lock +++ b/bun.lock @@ -5,14 +5,14 @@ "": { "name": "google-spreadsheet", "dependencies": { - "es-toolkit": "^1.44.0", - "ky": "^1.14.3", + "es-toolkit": "^1.46.1", + "ky": "^2.0.2", }, "devDependencies": { "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", - "@varlock/bumpy": "^1.5.1", + "@varlock/bumpy": "^1.6.0", "docsify-cli": "^4.4.4", "eslint": "^8.41.0", "eslint-config-airbnb-base": "^15.0.0", @@ -271,7 +271,7 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@varlock/bumpy": ["@varlock/bumpy@1.5.1", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-oSGpOR7OIDJ4UXyX8ZosUAQYYgvBR3BOdNj+WHYXY0DSEWyC4Lzh09iVC9pGflo3vpZ+OtyemGSyL2nWhz/Qdw=="], + "@varlock/bumpy": ["@varlock/bumpy@1.6.0", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-hrKJ3W897e/h9OkYAbk0J2rfEhC8Y+L5fvuZzAfwTG5r6Zxwa8n1gu1Pb0osuYuolubIr4EzHtDBP3mi9HotOA=="], "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], @@ -481,7 +481,7 @@ "es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], - "es-toolkit": ["es-toolkit@1.44.0", "", {}, "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg=="], + "es-toolkit": ["es-toolkit@1.46.1", "", {}, "sha512-5eNtXOs3tbfxXOj04tjjseeWkRWaoCjdEI+96DgwzZoe6c9juL49pXlzAFTI72aWC9Y8p7168g6XIKjh7k6pyQ=="], "esbuild": ["esbuild@0.27.3", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="], @@ -767,7 +767,7 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "ky": ["ky@1.14.3", "", {}, "sha512-9zy9lkjac+TR1c2tG+mkNSVlyOpInnWdSMiue4F+kq8TwJSgv6o8jhLRg8Ho6SnZ9wOYUq/yozts9qQCfk7bIw=="], + "ky": ["ky@2.0.2", "", {}, "sha512-/GmXpo9F9W+f8n4Ivr2iH+7h7wL7jLbLKWkMlpflcCRb6kGjBfTlASEXaZ9qUgNTn4VgS0P2pwxxzQ4EM6Ulgg=="], "latest-version": ["latest-version@5.1.0", "", { "dependencies": { "package-json": "^6.3.0" } }, "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA=="], diff --git a/package.json b/package.json index 4d81ece..947b9fd 100644 --- a/package.json +++ b/package.json @@ -51,14 +51,14 @@ "test:ci": "vitest run" }, "dependencies": { - "es-toolkit": "^1.44.0", - "ky": "^1.14.3" + "es-toolkit": "^1.46.1", + "ky": "^2.0.2" }, "devDependencies": { "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", - "@varlock/bumpy": "^1.5.1", + "@varlock/bumpy": "^1.6.0", "docsify-cli": "^4.4.4", "eslint": "^8.41.0", "eslint-config-airbnb-base": "^15.0.0", diff --git a/src/lib/GoogleSpreadsheet.ts b/src/lib/GoogleSpreadsheet.ts index 3bf942b..2826500 100644 --- a/src/lib/GoogleSpreadsheet.ts +++ b/src/lib/GoogleSpreadsheet.ts @@ -142,19 +142,23 @@ export class GoogleSpreadsheet { // create a ky instance with sheet root URL and hooks to handle auth this.sheetsApi = ky.create({ - prefixUrl: `${SHEETS_API_BASE_URL}/${spreadsheetId}`, + prefix: `${SHEETS_API_BASE_URL}/${spreadsheetId}`, timeout: 180_000, hooks: { - beforeRequest: [(r) => this._setAuthRequestHook(r)], - beforeError: [(e) => this._errorHook(e)], + beforeRequest: [ + ({ request }) => this._setAuthRequestHook(request), + ], + beforeError: [ + ({ error }) => this._errorHook(error), + ], }, retry: retryConfig, }); this.driveApi = ky.create({ - prefixUrl: `${DRIVE_API_BASE_URL}/${spreadsheetId}`, + prefix: `${DRIVE_API_BASE_URL}/${spreadsheetId}`, hooks: { - beforeRequest: [(r) => this._setAuthRequestHook(r)], - beforeError: [(e) => this._errorHook(e)], + beforeRequest: [({ request }) => this._setAuthRequestHook(request)], + beforeError: [({ error }) => this._errorHook(error)], }, retry: retryConfig, }); @@ -185,7 +189,9 @@ export class GoogleSpreadsheet { } /** @internal */ - async _errorHook(error: HTTPError) { + async _errorHook(error: Error) { + if (!(error instanceof HTTPError)) return error; + const { response } = error; const errorDataText = await response?.text(); let errorData; @@ -494,7 +500,7 @@ export class GoogleSpreadsheet { const exportUrl = this._spreadsheetUrl.replace('edit', 'export'); const response = await this.sheetsApi.get(exportUrl, { - prefixUrl: '', // unset baseUrl since we're not hitting the normal sheets API + prefix: '', // unset baseUrl since we're not hitting the normal sheets API searchParams: { id: this.spreadsheetId, format: fileType, diff --git a/src/test/exports.test.ts b/src/test/exports.test.ts new file mode 100644 index 0000000..2d572ce --- /dev/null +++ b/src/test/exports.test.ts @@ -0,0 +1,107 @@ +import { + describe, expect, it, beforeAll, afterAll, afterEach, +} from 'vitest'; +import { setTimeout as delay } from 'timers/promises'; +import { ENV } from 'varlock/env'; + +import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from '..'; + +import { DOC_IDS, testServiceAccountAuth } from './auth/docs-and-auth'; + +const doc = new GoogleSpreadsheet(DOC_IDS.private, testServiceAccountAuth); +let sheet: GoogleSpreadsheetWorksheet; + +describe('Export/download methods', () => { + beforeAll(async () => { + await doc.loadInfo(); + sheet = await doc.addSheet({ + title: `Export test ${+new Date()}`, + headerValues: ['name', 'value'], + }); + await sheet.addRows([ + { name: 'Alice', value: '100' }, + { name: 'Bob', value: '200' }, + { name: 'Charlie', value: '300' }, + ]); + }); + + afterAll(async () => { + await sheet.delete(); + }); + + // hitting rate limits when running tests on ci - so we add a short delay + if (ENV.TEST_DELAY) afterEach(async () => delay(ENV.TEST_DELAY)); + + describe('document-level exports', () => { + it('can download as XLSX', async () => { + const buffer = await doc.downloadAsXLSX(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + }); + + it('can download as XLSX stream', async () => { + const stream = await doc.downloadAsXLSX(true); + expect(stream).toBeTruthy(); + // ReadableStream should have a getReader method + expect(typeof (stream as ReadableStream).getReader).toBe('function'); + }); + + it('can download as ODS', async () => { + const buffer = await doc.downloadAsODS(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + }); + + it('can download as zipped HTML', async () => { + const buffer = await doc.downloadAsZippedHTML(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + }); + }); + + describe('worksheet-level exports', () => { + it('can download as CSV and verify content', async () => { + const buffer = await sheet.downloadAsCSV(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + + const csvText = new TextDecoder().decode(buffer); + const lines = csvText.trim().split('\n'); + + // header row + expect(lines[0]).toContain('name'); + expect(lines[0]).toContain('value'); + + // data rows + expect(lines[1]).toContain('Alice'); + expect(lines[1]).toContain('100'); + expect(lines[2]).toContain('Bob'); + expect(lines[2]).toContain('200'); + expect(lines[3]).toContain('Charlie'); + expect(lines[3]).toContain('300'); + }); + + it('can download as CSV stream', async () => { + const stream = await sheet.downloadAsCSV(true); + expect(stream).toBeTruthy(); + expect(typeof (stream as ReadableStream).getReader).toBe('function'); + }); + + it('can download as TSV', async () => { + const buffer = await sheet.downloadAsTSV(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + + const tsvText = new TextDecoder().decode(buffer); + // TSV uses tabs + expect(tsvText).toContain('\t'); + expect(tsvText).toContain('Alice'); + }); + + it('can download as PDF', async () => { + const buffer = await sheet.downloadAsPDF(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); + }); + }); +}); From ddc32b97f49380e337ff150f3ee6477cd2f29cff Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 29 Apr 2026 21:49:34 -0700 Subject: [PATCH 2/7] fix: handle consumed response body in ky error hook Clone the response before reading text in beforeError hook since ky may have already consumed the body, causing "Body has already been consumed" errors. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/GoogleSpreadsheet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/GoogleSpreadsheet.ts b/src/lib/GoogleSpreadsheet.ts index 2826500..7bf1c6b 100644 --- a/src/lib/GoogleSpreadsheet.ts +++ b/src/lib/GoogleSpreadsheet.ts @@ -193,12 +193,12 @@ export class GoogleSpreadsheet { if (!(error instanceof HTTPError)) return error; const { response } = error; - const errorDataText = await response?.text(); let errorData; try { + const errorDataText = await response?.clone().text(); errorData = JSON.parse(errorDataText); } catch (e) { - // console.log('parsing json failed', errorDataText); + // body may have already been consumed, or response may not be JSON } if (errorData) { From 867217549272ae1a9d73298068cb3a7c8b20beb9 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 29 Apr 2026 21:56:58 -0700 Subject: [PATCH 3/7] fix: use error.data instead of reading consumed response body in ky error hook New ky versions pre-parse the response body into error.data before beforeError hooks run, making response.text() fail. Use error.data directly to extract Google API error details. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/lib/GoogleSpreadsheet.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/src/lib/GoogleSpreadsheet.ts b/src/lib/GoogleSpreadsheet.ts index 7bf1c6b..a8f8d55 100644 --- a/src/lib/GoogleSpreadsheet.ts +++ b/src/lib/GoogleSpreadsheet.ts @@ -192,25 +192,18 @@ export class GoogleSpreadsheet { async _errorHook(error: Error) { if (!(error instanceof HTTPError)) return error; - const { response } = error; - let errorData; - try { - const errorDataText = await response?.clone().text(); - errorData = JSON.parse(errorDataText); - } catch (e) { - // body may have already been consumed, or response may not be JSON - } - - if (errorData) { - // usually the error has a code and message, but occasionally not - if (!errorData.error) return error; + // ky pre-parses the response body into error.data (the response body is already consumed) + const errorData = typeof error.data === 'string' ? (() => { + try { return JSON.parse(error.data as string); } catch { return undefined; } + })() : error.data; + if (errorData?.error) { const { code, message } = errorData.error; error.message = `Google API error - [${code}] ${message}`; return error; } - if (_.get(error, 'response.status') === 403) { + if (error.response?.status === 403) { if ('apiKey' in this.auth) { throw new Error('Sheet is private. Use authentication or make public. (see https://github.com/theoephraim/node-google-spreadsheet#a-note-on-authentication for details)'); } From fb6d7a82e43bde2f46ca90276f17c27ce8d146d8 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Wed, 29 Apr 2026 23:14:46 -0700 Subject: [PATCH 4/7] increase TEST_DELAY on CI to reduce rate limiting flakiness Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.schema | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.env.schema b/.env.schema index 4f321a7..5a94bdb 100644 --- a/.env.schema +++ b/.env.schema @@ -24,4 +24,4 @@ CI= # Delay in ms to use when running tests # @type=number -TEST_DELAY=200 \ No newline at end of file +TEST_DELAY=if($CI, 300, 200) \ No newline at end of file From 99272993594434130982044cf5a292a582364225 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Thu, 21 May 2026 12:05:02 -0700 Subject: [PATCH 5/7] bump dev deps and simplify export tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bump @varlock/bumpy and varlock - simplify export tests to avoid rate limiting (8 → 4 tests, 3s delay) Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 8 ++-- package.json | 4 +- src/test/exports.test.ts | 97 +++++++++++++--------------------------- 3 files changed, 36 insertions(+), 73 deletions(-) diff --git a/bun.lock b/bun.lock index ddad1b5..f0c34b9 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", - "@varlock/bumpy": "^1.6.0", + "@varlock/bumpy": "^1.8.1", "docsify-cli": "^4.4.4", "eslint": "^8.41.0", "eslint-config-airbnb-base": "^15.0.0", @@ -23,7 +23,7 @@ "nock": "^14.0.11", "tsdown": "^0.20.3", "typescript": "^5.9.3", - "varlock": "^1.0.0", + "varlock": "^1.1.0", "vitest": "^4.0.18", }, "peerDependencies": { @@ -271,7 +271,7 @@ "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - "@varlock/bumpy": ["@varlock/bumpy@1.6.0", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-hrKJ3W897e/h9OkYAbk0J2rfEhC8Y+L5fvuZzAfwTG5r6Zxwa8n1gu1Pb0osuYuolubIr4EzHtDBP3mi9HotOA=="], + "@varlock/bumpy": ["@varlock/bumpy@1.8.1", "", { "bin": { "bumpy": "dist/cli.mjs" } }, "sha512-HhJ4UYeCPAMtSWYKO9lc5RABjpHjLZdu1lEzCJbgEWo+QOz2JCRcxpDLeH6XQJ1oQddrxUQeSiqcuuElJoCVEw=="], "@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="], @@ -1121,7 +1121,7 @@ "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "varlock": ["varlock@1.0.0", "", { "bin": { "varlock": "bin/cli.js" } }, "sha512-0KwOc6x+daO4BOG2kuLVDsbo3+m2avCsEH1ZAccRh/dvr9UliejleD49LHhxLXiVv7kyBgCxARWI89F/EjBwIQ=="], + "varlock": ["varlock@1.1.0", "", { "bin": { "varlock": "bin/cli.js" } }, "sha512-S6wE06TzoGBtVM0CKHN1Z4D9lW6eA3fQ8RmsPx6gZ/nkQtye19fel0gvpPMk37YOCEtHY/vaxkMIVziX6NRFCg=="], "vite": ["vite@7.3.1", "", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA=="], diff --git a/package.json b/package.json index 947b9fd..da71c89 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ "@types/node": "^25.2.3", "@typescript-eslint/eslint-plugin": "^5.59.7", "@typescript-eslint/parser": "^5.59.7", - "@varlock/bumpy": "^1.6.0", + "@varlock/bumpy": "^1.8.1", "docsify-cli": "^4.4.4", "eslint": "^8.41.0", "eslint-config-airbnb-base": "^15.0.0", @@ -69,7 +69,7 @@ "nock": "^14.0.11", "tsdown": "^0.20.3", "typescript": "^5.9.3", - "varlock": "^1.0.0", + "varlock": "^1.1.0", "vitest": "^4.0.18" }, "peerDependencies": { diff --git a/src/test/exports.test.ts b/src/test/exports.test.ts index 2d572ce..070f67c 100644 --- a/src/test/exports.test.ts +++ b/src/test/exports.test.ts @@ -2,7 +2,6 @@ import { describe, expect, it, beforeAll, afterAll, afterEach, } from 'vitest'; import { setTimeout as delay } from 'timers/promises'; -import { ENV } from 'varlock/env'; import { GoogleSpreadsheet, GoogleSpreadsheetWorksheet } from '..'; @@ -29,79 +28,43 @@ describe('Export/download methods', () => { await sheet.delete(); }); - // hitting rate limits when running tests on ci - so we add a short delay - if (ENV.TEST_DELAY) afterEach(async () => delay(ENV.TEST_DELAY)); + // export endpoint has tight rate limits + afterEach(async () => delay(3000)); - describe('document-level exports', () => { - it('can download as XLSX', async () => { - const buffer = await doc.downloadAsXLSX(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); - }); - - it('can download as XLSX stream', async () => { - const stream = await doc.downloadAsXLSX(true); - expect(stream).toBeTruthy(); - // ReadableStream should have a getReader method - expect(typeof (stream as ReadableStream).getReader).toBe('function'); - }); - - it('can download as ODS', async () => { - const buffer = await doc.downloadAsODS(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); - }); - - it('can download as zipped HTML', async () => { - const buffer = await doc.downloadAsZippedHTML(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); - }); + it('can download document as XLSX', async () => { + const buffer = await doc.downloadAsXLSX(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); }); - describe('worksheet-level exports', () => { - it('can download as CSV and verify content', async () => { - const buffer = await sheet.downloadAsCSV(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); - - const csvText = new TextDecoder().decode(buffer); - const lines = csvText.trim().split('\n'); + it('can download worksheet as CSV and verify content', async () => { + const buffer = await sheet.downloadAsCSV(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); - // header row - expect(lines[0]).toContain('name'); - expect(lines[0]).toContain('value'); + const csvText = new TextDecoder().decode(buffer); + const lines = csvText.trim().split('\n'); - // data rows - expect(lines[1]).toContain('Alice'); - expect(lines[1]).toContain('100'); - expect(lines[2]).toContain('Bob'); - expect(lines[2]).toContain('200'); - expect(lines[3]).toContain('Charlie'); - expect(lines[3]).toContain('300'); - }); - - it('can download as CSV stream', async () => { - const stream = await sheet.downloadAsCSV(true); - expect(stream).toBeTruthy(); - expect(typeof (stream as ReadableStream).getReader).toBe('function'); - }); + expect(lines[0]).toContain('name'); + expect(lines[0]).toContain('value'); + expect(lines[1]).toContain('Alice'); + expect(lines[2]).toContain('Bob'); + expect(lines[3]).toContain('Charlie'); + }); - it('can download as TSV', async () => { - const buffer = await sheet.downloadAsTSV(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); + it('can download worksheet as TSV', async () => { + const buffer = await sheet.downloadAsTSV(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); - const tsvText = new TextDecoder().decode(buffer); - // TSV uses tabs - expect(tsvText).toContain('\t'); - expect(tsvText).toContain('Alice'); - }); + const tsvText = new TextDecoder().decode(buffer); + expect(tsvText).toContain('\t'); + expect(tsvText).toContain('Alice'); + }); - it('can download as PDF', async () => { - const buffer = await sheet.downloadAsPDF(); - expect(buffer).toBeInstanceOf(ArrayBuffer); - expect(buffer.byteLength).toBeGreaterThan(0); - }); + it('can download worksheet as PDF', async () => { + const buffer = await sheet.downloadAsPDF(); + expect(buffer).toBeInstanceOf(ArrayBuffer); + expect(buffer.byteLength).toBeGreaterThan(0); }); }); From ba0d1f1839fb0aa3f6e9e95f3e01e83fa15f3612 Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Thu, 21 May 2026 12:06:35 -0700 Subject: [PATCH 6/7] bump version to minor for ky v2 upgrade Co-Authored-By: Claude Opus 4.6 (1M context) --- .bumpy/update-deps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.bumpy/update-deps.md b/.bumpy/update-deps.md index 33ca250..d80dd0e 100644 --- a/.bumpy/update-deps.md +++ b/.bumpy/update-deps.md @@ -1,5 +1,5 @@ --- -google-spreadsheet: patch +google-spreadsheet: minor --- bump deps From e2c8763295844827015700f3a7fa03b23d98757d Mon Sep 17 00:00:00 2001 From: Theo Ephraim Date: Thu, 21 May 2026 12:06:53 -0700 Subject: [PATCH 7/7] add ky v2 breaking change notes to bump file Co-Authored-By: Claude Opus 4.6 (1M context) --- .bumpy/update-deps.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.bumpy/update-deps.md b/.bumpy/update-deps.md index d80dd0e..bf399bb 100644 --- a/.bumpy/update-deps.md +++ b/.bumpy/update-deps.md @@ -2,4 +2,6 @@ google-spreadsheet: minor --- -bump deps +upgrade ky from v1 to v2 + +If you use `doc.sheetsApi` or `doc.driveApi` directly, note that ky v2 changed its hook signatures (hooks now receive state objects instead of direct Request/Error params) and renamed `prefixUrl` to `prefix`. See the [ky v2 migration guide](https://github.com/sindresorhus/ky/releases/tag/v2.0.0) for details.