From 67bfc4ea3eab27e8747afcb9649ce5b8b497f322 Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Wed, 7 Feb 2024 22:30:45 +0900 Subject: [PATCH 1/6] Add `eslint-plugin-jest` to `devDependencies` --- package-lock.json | 169 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 170 insertions(+) diff --git a/package-lock.json b/package-lock.json index a94f31e..b64d22f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "eslint": "^8.47.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-googleappsscript": "^1.0.5", + "eslint-plugin-jest": "^27.6.3", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", "prettier": "^3.0.3", @@ -3039,6 +3040,153 @@ "node": ">=0.10.0" } }, + "node_modules/eslint-plugin-jest": { + "version": "27.6.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.3.tgz", + "integrity": "sha512-+YsJFVH6R+tOiO3gCJon5oqn4KWc+mDq2leudk8mrp8RFubLOo9CVyi3cib4L7XMpxExmkmBZQTPDYVBzgpgOA==", + "dev": true, + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-plugin-jest/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, "node_modules/eslint-plugin-prettier": { "version": "5.1.3", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", @@ -7345,6 +7493,27 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index b6fa8b5..847b011 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "eslint": "^8.47.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-googleappsscript": "^1.0.5", + "eslint-plugin-jest": "^27.6.3", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.7.0", "prettier": "^3.0.3", From eeddf75e1640d8ef66b67da5802d29d7504e96b4 Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Wed, 7 Feb 2024 22:31:17 +0900 Subject: [PATCH 2/6] Update .eslintrc.yml to add `overrides` values --- .eslintrc.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.eslintrc.yml b/.eslintrc.yml index 4a51fe3..f61c3ea 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -21,6 +21,12 @@ parserOptions: project: true ecmaVersion: 2018 sourceType: module +overrides: + - files: ['tests/**/*.ts'] + plugins: ['jest'] + rules: + '@typescript-eslint/unbound-method': 'off' + 'jest/unbound-method': 'error' ########### # Plugins # From 10d36d37cfa68e6e8bbccd4d5f30fe5746cc919d Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Wed, 7 Feb 2024 22:31:44 +0900 Subject: [PATCH 3/6] Add `tests/**/*.ts` to `include` in tsconfig.json --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 2b3161e..d52bccb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -106,5 +106,5 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "tests/**/*.ts"] } From e1a5ef6113e0555d6416e5cfc65cb188b6fd544d Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Wed, 7 Feb 2024 22:36:39 +0900 Subject: [PATCH 4/6] Updates to fix strict type check errors in eslint --- src/sheetsl.ts | 60 ++++++++++++++++++++++++-------------------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/src/sheetsl.ts b/src/sheetsl.ts index 96e05bd..7242751 100644 --- a/src/sheetsl.ts +++ b/src/sheetsl.ts @@ -29,29 +29,29 @@ const THRESHOLD_BYTES = 1900; * GET request on /v2/languages returns an array of this object. * @see https://www.deepl.com/docs-api/general/get-languages/ */ -export type DeepLSupportedLanguages = { +export interface DeepLSupportedLanguages { language: string; name: string; supports_formality: boolean; -}; +} /** * The response from the DeepL API for POST /v2/translate. * @see https://www.deepl.com/docs-api/translate-text/ */ -type DeepLTranslationResponse = { +interface DeepLTranslationResponse { translations: DeepLTranslationObj[]; -}; +} /** * The individual translated text object in the translated response * from DeepL API. * @see https://www.deepl.com/docs-api/translate-text/ */ -type DeepLTranslationObj = { +interface DeepLTranslationObj { detected_source_language: string; text: string; -}; +} /** * The type of language that should be returned in the GET request @@ -64,9 +64,7 @@ export type DeepLLanguageType = 'source' | 'target'; * The type of the object containing key-values pairs to set in the properties of the Google Apps Script. * @see https://developers.google.com/apps-script/reference/properties/properties#setpropertiesproperties */ -type PropertiesObj = { - [key: string]: string; -}; +type PropertiesObj = Record; /** * Create add-on menu on opening spreadsheet file. @@ -77,8 +75,8 @@ function onOpen(): void { .addSubMenu( ui .createMenu('Settings') - .addItem('Set Auth Key', 'setDeeplAuthKey') - .addItem('Delete Auth Key', 'deleteDeeplAuthKey') + .addItem('Set DeepL API Key', 'setDeeplApiKey') + .addItem('Delete DeepL API Key', 'deleteDeeplApiKey') .addSeparator() .addItem('Set Language', 'setLanguage'), ) @@ -97,14 +95,14 @@ export function onInstall(): void { /** * Store DeepL API authentication key in user property. */ -export function setDeeplAuthKey(): void { +export function setDeeplApiKey(): void { const ui = SpreadsheetApp.getUi(); try { const promptResponse = ui.prompt( 'Enter your DeepL API Authentication Key', ui.ButtonSet.OK_CANCEL, ); - const apiKey = verifyAuthKeyPrompt(promptResponse, ui).getResponseText(); + const apiKey = verifyApiKeyPrompt(promptResponse, ui).getResponseText(); PropertiesService.getUserProperties().setProperty( UP_KEY_DEEPL_API_KEY, apiKey, @@ -119,14 +117,14 @@ export function setDeeplAuthKey(): void { } /** - * Verify the prompt response in setDeeplAuthKey and return an error + * Verify the prompt response in setDeeplApiKey and return an error * if the prompt is canceled or if an invalid DeepL API Authentication Key * was entered. - * @param promptResponse Response object for the user prompt in setDeeplAuthKey + * @param promptResponse Response object for the user prompt in setDeeplApiKey * to enter the user's DeepL API Authentication Key. * @returns The entered prompt response object. */ -export function verifyAuthKeyPrompt( +export function verifyApiKeyPrompt( promptResponse: GoogleAppsScript.Base.PromptResponse, ui: GoogleAppsScript.Base.Ui, ): GoogleAppsScript.Base.PromptResponse { @@ -147,7 +145,7 @@ export function verifyAuthKeyPrompt( /** * Delete the stored DeepL API authentication key in user property. */ -export function deleteDeeplAuthKey(): void { +export function deleteDeeplApiKey(): void { const ui = SpreadsheetApp.getUi(); try { PropertiesService.getUserProperties().deleteProperty(UP_KEY_DEEPL_API_KEY); @@ -241,7 +239,7 @@ export function setLanguage(): void { ); } // Set the values as user properties - let setObj: PropertiesObj = {}; + const setObj: PropertiesObj = {}; setObj[UP_KEY_SOURCE_LOCALE] = responseSourceLocale; setObj[UP_KEY_TARGET_LOCALE] = responseTargetLocale; up.setProperties(setObj, false); @@ -372,14 +370,13 @@ export function deepLTranslate( // console.log(`url: ${url}`); // Call the DeepL API translate request - const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true }); - - // Handle DeepL API errors - handleDeepLErrors(response); + const response = handleDeepLErrors( + UrlFetchApp.fetch(url, { muteHttpExceptions: true }), + ); - const translatedTextObj: DeepLTranslationResponse = JSON.parse( + const translatedTextObj = JSON.parse( response.getContentText(), - ); + ) as DeepLTranslationResponse; const translatedText: string[] = translatedTextObj.translations.map( (translationsResponse: DeepLTranslationObj): string => translationsResponse.text, @@ -404,13 +401,12 @@ export function deepLGetLanguages( const apiKey = getDeepLApiKey(); const baseUrl = getDeepLApiBaseUrl(apiKey); // Call the DeepL API - let url = baseUrl + endpoint + `?auth_key=${apiKey}&type=${type}`; - const response = UrlFetchApp.fetch(url, { muteHttpExceptions: true }); - - // Handle DeepL API errors - handleDeepLErrors(response); + const url = baseUrl + endpoint + `?auth_key=${apiKey}&type=${type}`; + const response = handleDeepLErrors( + UrlFetchApp.fetch(url, { muteHttpExceptions: true }), + ); - return JSON.parse(response.getContentText()); + return JSON.parse(response.getContentText()) as DeepLSupportedLanguages[]; } /** @@ -424,12 +420,13 @@ export function getBlobBytes(text: string): number { /** * Handle DeepL API errors based on the response code. + * Returns the entered response if the response code is 200. * @param response The UrlFetchApp.fetch response from the DeepL API * @see https://www.deepl.com/docs-api/api-access/error-handling/ */ export function handleDeepLErrors( response: GoogleAppsScript.URL_Fetch.HTTPResponse, -): void { +): GoogleAppsScript.URL_Fetch.HTTPResponse { const responseCode = response.getResponseCode(); if (responseCode === 429) { throw new Error( @@ -448,6 +445,7 @@ export function handleDeepLErrors( `[${ADDON_NAME}] Error on Calling DeepL API: ${response.getContentText()}`, ); } + return response; } /** From b7ef3ed8abf8fcd79846971bb938da76d172b79d Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Wed, 7 Feb 2024 22:38:36 +0900 Subject: [PATCH 5/6] Add new tests and refactor existing ones resolve #86 and #87 --- tests/deepL.test.ts | 213 ------------------------------ tests/deepLGetLanguages.test.ts | 35 +++++ tests/deleteDeeplApiKey.test.ts | 41 ++++++ tests/getBlobBytes.test.ts | 15 +++ tests/getBytes.test.ts | 20 --- tests/getDeepLApiKey.test.ts | 42 +++--- tests/handleDeepLErrors.test.ts | 132 +++++++++--------- tests/onInstall_onOpen.test.ts | 22 +++ tests/onInstallonOpen.test.ts | 96 -------------- tests/setDeeplApiKey.test.ts | 61 +++++++++ tests/verifyAuthKeyPrompt.test.ts | 148 +++++++-------------- 11 files changed, 315 insertions(+), 510 deletions(-) delete mode 100644 tests/deepL.test.ts create mode 100644 tests/deepLGetLanguages.test.ts create mode 100644 tests/deleteDeeplApiKey.test.ts create mode 100644 tests/getBlobBytes.test.ts delete mode 100644 tests/getBytes.test.ts create mode 100644 tests/onInstall_onOpen.test.ts delete mode 100644 tests/onInstallonOpen.test.ts create mode 100644 tests/setDeeplApiKey.test.ts diff --git a/tests/deepL.test.ts b/tests/deepL.test.ts deleted file mode 100644 index d7ccd65..0000000 --- a/tests/deepL.test.ts +++ /dev/null @@ -1,213 +0,0 @@ -// Jest tests for deepLTranslate() and deepLGetLanguages() - -import { - deepLTranslate, - deepLGetLanguages, - DeepLSupportedLanguages, - DeepLLanguageType, -} from '../src/sheetsl'; - -const ADDON_NAME = 'SheetsL'; -const DEEPL_API_BASE_URL_FREE = 'https://api-free.deepl.com/v2/'; -const translateUrl = DEEPL_API_BASE_URL_FREE + 'translate'; -const getLanguageUrl = DEEPL_API_BASE_URL_FREE + 'languages'; -const mockApiKey = 'apiKeyString:fx'; - -PropertiesService.getUserProperties = jest.fn(() => ({ - getProperty: jest.fn(() => mockApiKey), -})) as any; - -UrlFetchApp.fetch = jest.fn((url: string, options: UrlFetchAppOptions) => ({ - _options: options, - getResponseCode: jest.fn((): number => 200), - getContentText: jest.fn((text: string = url) => { - // Switch the returning value of the mock function for UrlFetchApp.fetch - // depending on the input URL. - if (text.startsWith(translateUrl)) { - // URL with the endpoint of DeepL API translation will return - // a stringified object of type DeepLTranslationResponse - return JSON.stringify({ - translations: [ - { - detected_source_language: 'JA', - text: text, // This mock translated text will return the string of the input URL for UrlFetchApp.fetch - }, - ], - }); - } else if (text.startsWith(getLanguageUrl)) { - // URL with the endpoint of DeepL API to retrieve the list of supported languages - // will return a stringified list of type DeepLSupportedLanguages objects. - return JSON.stringify([ - { - language: 'EN-US', - name: 'English (American)', - supports_formality: false, - }, - { - language: 'JA', - name: 'Japanese', - supports_formality: false, - }, - { - language: 'MOCK', - name: text, // This mock language will return the string of the input URL for UrlFetchApp.fetch - supports_formality: true, - }, - ]); - } - }), -})) as any; - -type UrlFetchAppOptions = { - muteHttpExceptions: boolean; -}; - -type DeepLTranslatePattern = { - title: string; - input: DeepLTranslatePatternInput; - expectedOutput: string[]; -}; - -type DeepLTranslatePatternInput = { - sourceText: string | string[]; - sourceLocale: string | null | undefined; - targetLocale: string; -}; - -type DeepLGetLanguages = { - title: string; - input: DeepLLanguageType; - expectedOutput: DeepLSupportedLanguages[]; -}; - -const deepLTranslatePatterns: DeepLTranslatePattern[] = [ - { - title: 'sourceText as string', - input: { - sourceText: 'text to translate', - sourceLocale: 'JA', - targetLocale: 'EN-US', - }, - expectedOutput: [ - `${translateUrl}?auth_key=${mockApiKey}&target_lang=EN-US&text=${encodeURIComponent( - 'text to translate', - )}&source_lang=JA`, - ], - }, - { - title: 'sourceText as an array of strings', - input: { - sourceText: [ - 'text to translate 1', - 'text to translate 2', - 'text to translate 3', - ], - sourceLocale: 'JA', - targetLocale: 'EN-US', - }, - expectedOutput: [ - `${translateUrl}?auth_key=${mockApiKey}&target_lang=EN-US&text=${encodeURIComponent( - 'text to translate 1', - )}&text=${encodeURIComponent( - 'text to translate 2', - )}&text=${encodeURIComponent('text to translate 3')}&source_lang=JA`, - ], - }, -]; - -const deepLTranslatePatternsWithErrors: DeepLTranslatePattern[] = [ - { - title: 'Empty sourceText', - input: { - sourceText: '', - sourceLocale: 'JA', - targetLocale: 'EN-US', - }, - expectedOutput: ['returns an error'], - }, -]; - -const deepLGetLanguagesPatterns: DeepLGetLanguages[] = [ - { - title: 'type = source', - input: 'source', - expectedOutput: [ - { - language: 'EN-US', - name: 'English (American)', - supports_formality: false, - }, - { - language: 'JA', - name: 'Japanese', - supports_formality: false, - }, - { - language: 'MOCK', - name: `${getLanguageUrl}?auth_key=${mockApiKey}&type=source`, - supports_formality: true, - }, - ], - }, - { - title: 'type = target', - input: 'target', - expectedOutput: [ - { - language: 'EN-US', - name: 'English (American)', - supports_formality: false, - }, - { - language: 'JA', - name: 'Japanese', - supports_formality: false, - }, - { - language: 'MOCK', - name: `${getLanguageUrl}?auth_key=${mockApiKey}&type=target`, - supports_formality: true, - }, - ], - }, -]; - -describe.each(deepLTranslatePatterns)( - 'deepLTranslate', - ({ title, input, expectedOutput }) => { - test(`deepLTranslate test: ${title}`, () => { - expect( - deepLTranslate( - input.sourceText, - input.sourceLocale, - input.targetLocale, - ), - ).toEqual(expectedOutput); - }); - }, -); - -// Error patterns in deepLTranslate -describe.each(deepLTranslatePatternsWithErrors)( - 'deepLTranslate Errors', - ({ title, input }) => { - test(`deepLTranslate error test: ${title}`, () => { - expect(() => { - deepLTranslate( - input.sourceText, - input.sourceLocale, - input.targetLocale, - ); - }).toThrowError(new Error(`[${ADDON_NAME}] Empty input.`)); - }); - }, -); - -describe.each(deepLGetLanguagesPatterns)( - 'deepLGetLanguages', - ({ title, input, expectedOutput }) => { - test(`deepLGetLanguages test: ${title}`, () => { - expect(deepLGetLanguages(input)).toEqual(expectedOutput); - }); - }, -); diff --git a/tests/deepLGetLanguages.test.ts b/tests/deepLGetLanguages.test.ts new file mode 100644 index 0000000..ce446a7 --- /dev/null +++ b/tests/deepLGetLanguages.test.ts @@ -0,0 +1,35 @@ +import { deepLGetLanguages } from '../src/sheetsl'; + +describe('deepLGetLanguages', () => { + beforeEach(() => { + global.UrlFetchApp = { + fetch: jest.fn(() => ({ + getContentText: jest.fn(() => + JSON.stringify({ + languages: [ + { language: 'EN', name: 'English', supportsFormality: true }, + { language: 'DE', name: 'German', supportsFormality: true }, + ], + }), + ), + getResponseCode: jest.fn(() => 200), + })), + } as unknown as GoogleAppsScript.URL_Fetch.UrlFetchApp; + global.PropertiesService = { + getUserProperties: jest.fn(() => ({ + getProperty: jest.fn(() => 'SampleApiKey:fx'), + })), + } as unknown as GoogleAppsScript.Properties.PropertiesService; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should return the list of supported languages', () => { + expect(deepLGetLanguages()).toEqual({ + languages: [ + { language: 'EN', name: 'English', supportsFormality: true }, + { language: 'DE', name: 'German', supportsFormality: true }, + ], + }); + }); +}); diff --git a/tests/deleteDeeplApiKey.test.ts b/tests/deleteDeeplApiKey.test.ts new file mode 100644 index 0000000..c02e576 --- /dev/null +++ b/tests/deleteDeeplApiKey.test.ts @@ -0,0 +1,41 @@ +import { deleteDeeplApiKey } from '../src/sheetsl'; + +describe('deleteDeeplApiKey', () => { + beforeEach(() => { + class MockUi { + // eslint-disable-next-line @typescript-eslint/no-empty-function + constructor() {} + alert: jest.Mock = jest.fn(); + } + global.SpreadsheetApp = { + getUi: jest.fn(() => new MockUi() as unknown as GoogleAppsScript.Base.Ui), + } as unknown as GoogleAppsScript.Spreadsheet.SpreadsheetApp; + // eslint-disable-next-line @typescript-eslint/no-empty-function + jest.spyOn(console, 'error').mockImplementation(() => {}); + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should successfully delete the DeepL API key', () => { + global.PropertiesService = { + getUserProperties: jest.fn(() => ({ + deleteProperty: jest.fn(), + })), + } as unknown as GoogleAppsScript.Properties.PropertiesService; + deleteDeeplApiKey(); + expect(PropertiesService.getUserProperties).toHaveBeenCalled(); + expect(console.error).not.toHaveBeenCalled(); + }); + it('should catch an error if something went wrong', () => { + global.PropertiesService = { + getUserProperties: jest.fn(() => ({ + deleteProperty: jest.fn(() => { + throw new Error('Test error'); + }), + })), + } as unknown as GoogleAppsScript.Properties.PropertiesService; + deleteDeeplApiKey(); + expect(PropertiesService.getUserProperties).toHaveBeenCalled(); + expect(console.error).toHaveBeenCalled(); + }); +}); diff --git a/tests/getBlobBytes.test.ts b/tests/getBlobBytes.test.ts new file mode 100644 index 0000000..a49a3b0 --- /dev/null +++ b/tests/getBlobBytes.test.ts @@ -0,0 +1,15 @@ +import { getBlobBytes } from '../src/sheetsl'; + +global.Utilities = { + newBlob: jest.fn((text: string) => ({ + getBytes: jest.fn(() => ({ + length: text.length, + })), + })), +} as unknown as GoogleAppsScript.Utilities.Utilities; + +describe('getBlobBytes', () => { + it('should return the length of the given string in bytes', () => { + expect(getBlobBytes('test string')).toBe(11); + }); +}); diff --git a/tests/getBytes.test.ts b/tests/getBytes.test.ts deleted file mode 100644 index fda10b1..0000000 --- a/tests/getBytes.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { getBlobBytes } from '../src/sheetsl'; - -Utilities.newBlob = jest.fn((text: string) => ({ - getBytes: jest.fn(() => ({ - length: text.length, - })), -})) as any; - -const patterns = [ - { - input: 'test string', - expectedOutput: 11, - }, -]; - -describe.each(patterns)('getBlobBytes', ({ input, expectedOutput }) => { - test(`getBlobBytes test: ${input}`, () => { - expect(getBlobBytes(input)).toBe(expectedOutput); - }); -}); diff --git a/tests/getDeepLApiKey.test.ts b/tests/getDeepLApiKey.test.ts index 61b2e04..fe9c5df 100644 --- a/tests/getDeepLApiKey.test.ts +++ b/tests/getDeepLApiKey.test.ts @@ -1,21 +1,29 @@ import { getDeepLApiKey } from '../src/sheetsl'; -const ADDON_NAME = 'SheetsL'; - -PropertiesService.getUserProperties = jest.fn(() => ({ - getProperty: jest.fn(() => undefined), -})) as any; - -const testObj = { - title: - 'Case when DeepL API Authentication Key is not saved in the user property', - errorMessage: `[${ADDON_NAME}] API Key Unavailable: Set the DeepL API Authentication Key from the Settings > Set Auth Key of the add-on menu.`, -}; - -describe('getDeepLApiKey Error', () => { - test(testObj.title, () => { - expect(() => { - getDeepLApiKey(); - }).toThrowError(new Error(testObj.errorMessage)); +describe('getDeepLApiKey', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should get the DeepL API key from the user properties and return it', () => { + const mockApiKey = 'Sample-API-key:fx'; + global.PropertiesService = { + getUserProperties: jest.fn(() => ({ + getProperty: jest.fn(() => mockApiKey), + })), + } as unknown as GoogleAppsScript.Properties.PropertiesService; + const result = getDeepLApiKey(); + expect(result).toBe(mockApiKey); + }); + it('should throw an error if the DeepL API key is not saved in the user properties', () => { + global.PropertiesService = { + getUserProperties: jest.fn(() => ({ + getProperty: jest.fn(() => null), + })), + } as unknown as GoogleAppsScript.Properties.PropertiesService; + expect(() => getDeepLApiKey()).toThrow( + new Error( + '[SheetsL] API Key Unavailable: Set the DeepL API Authentication Key from the Settings > Set Auth Key of the add-on menu.', + ), + ); }); }); diff --git a/tests/handleDeepLErrors.test.ts b/tests/handleDeepLErrors.test.ts index fc4de09..8d074d3 100644 --- a/tests/handleDeepLErrors.test.ts +++ b/tests/handleDeepLErrors.test.ts @@ -1,70 +1,70 @@ import { handleDeepLErrors } from '../src/sheetsl'; -const ADDON_NAME = 'SheetsL'; - -const successPattern = { - title: 'HTTP Response Code 200', - inputResponse: { - getResponseCode: () => 200, - getContentText: () => 'Testing HTTP Response Code 200', - }, -} as any; -const errorPatterns = [ - { - title: 'HTTP Response Code 429', - inputResponse: { - getResponseCode: () => 429, - getContentText: () => 'Testing HTTP Response Code 429', - }, - expectedErrorMessage: `[${ADDON_NAME}] Too Many Requests: Try again after some time.`, - }, - { - title: 'HTTP Response Code 456', - inputResponse: { - getResponseCode: () => 456, - getContentText: () => 'Testing HTTP Response Code 456', - }, - expectedErrorMessage: `[${ADDON_NAME}] Quota Exceeded: The translation limit of your account has been reached.`, - }, - { - title: 'HTTP Response Code 500', - inputResponse: { - getResponseCode: () => 500, - getContentText: () => 'Testing HTTP Response Code 500', - }, - expectedErrorMessage: `[${ADDON_NAME}] Temporary errors in the DeepL service. Please retry after waiting for a while.`, - }, - { - title: 'HTTP Response Code 501', - inputResponse: { - getResponseCode: () => 501, - getContentText: () => 'Testing HTTP Response Code 501', - }, - expectedErrorMessage: `[${ADDON_NAME}] Temporary errors in the DeepL service. Please retry after waiting for a while.`, - }, - { - title: 'HTTP Response Code 300', - inputResponse: { - getResponseCode: () => 300, - getContentText: () => 'Testing HTTP Response Code 300', - }, - expectedErrorMessage: `[${ADDON_NAME}] Error on Calling DeepL API: Testing HTTP Response Code 300`, - }, -] as any[]; - -describe('handleDeepLErrors Success', () => { - test(successPattern.title, () => { - expect(handleDeepLErrors(successPattern.inputResponse)).toBeUndefined(); +describe('handleDeepLErrors', () => { + it('should return the entered response if the response code is 200', () => { + const mockResponse = { + getResponseCode: () => 200, + getContentText: () => 'Testing HTTP Response Code 200', + } as unknown as GoogleAppsScript.URL_Fetch.HTTPResponse; + const result = handleDeepLErrors(mockResponse); + expect(result).toEqual(mockResponse); + }); + describe('handleDeepLErrors Error', () => { + const errorPatterns = [ + { + title: 'HTTP Response Code 429', + inputResponse: { + getResponseCode: () => 429, + getContentText: () => 'Testing HTTP Response Code 429', + } as unknown as GoogleAppsScript.URL_Fetch.HTTPResponse, + expectedErrorMessage: + '[SheetsL] Too Many Requests: Try again after some time.', + }, + { + title: 'HTTP Response Code 456', + inputResponse: { + getResponseCode: () => 456, + getContentText: () => 'Testing HTTP Response Code 456', + } as unknown as GoogleAppsScript.URL_Fetch.HTTPResponse, + expectedErrorMessage: + '[SheetsL] Quota Exceeded: The translation limit of your account has been reached.', + }, + { + title: 'HTTP Response Code 500', + inputResponse: { + getResponseCode: () => 500, + getContentText: () => 'Testing HTTP Response Code 500', + } as unknown as GoogleAppsScript.URL_Fetch.HTTPResponse, + expectedErrorMessage: + '[SheetsL] Temporary errors in the DeepL service. Please retry after waiting for a while.', + }, + { + title: 'HTTP Response Code 501', + inputResponse: { + getResponseCode: () => 501, + getContentText: () => 'Testing HTTP Response Code 501', + } as unknown as GoogleAppsScript.URL_Fetch.HTTPResponse, + expectedErrorMessage: + '[SheetsL] Temporary errors in the DeepL service. Please retry after waiting for a while.', + }, + { + title: 'HTTP Response Code 300', + inputResponse: { + getResponseCode: () => 300, + getContentText: () => 'Testing HTTP Response Code 300', + } as unknown as GoogleAppsScript.URL_Fetch.HTTPResponse, + expectedErrorMessage: + '[SheetsL] Error on Calling DeepL API: Testing HTTP Response Code 300', + }, + ]; + it.each(errorPatterns)( + 'should throw an error when the $title', + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ({ title, inputResponse, expectedErrorMessage }) => { + expect(() => handleDeepLErrors(inputResponse)).toThrow( + new Error(expectedErrorMessage), + ); + }, + ); }); }); - -describe.each(errorPatterns)( - 'handleDeepLErrors Errors', - ({ title, inputResponse, expectedErrorMessage }) => { - test(title, () => { - expect(() => { - handleDeepLErrors(inputResponse); - }).toThrowError(new Error(expectedErrorMessage)); - }); - }, -); diff --git a/tests/onInstall_onOpen.test.ts b/tests/onInstall_onOpen.test.ts new file mode 100644 index 0000000..80dfaa5 --- /dev/null +++ b/tests/onInstall_onOpen.test.ts @@ -0,0 +1,22 @@ +import { onInstall } from '../src/sheetsl'; + +class MockUi { + // eslint-disable-next-line @typescript-eslint/no-empty-function + constructor() {} + createAddonMenu: jest.Mock = jest.fn(() => this); + createMenu: jest.Mock = jest.fn(() => this); + addItem: jest.Mock = jest.fn(() => this); + addSeparator: jest.Mock = jest.fn(() => this); + addSubMenu: jest.Mock = jest.fn(() => this); + addToUi: jest.Mock = jest.fn(); +} + +describe('onInstall and onOpen', () => { + it('should create the add-on menu', () => { + global.SpreadsheetApp = { + getUi: jest.fn(() => new MockUi()), + } as unknown as GoogleAppsScript.Spreadsheet.SpreadsheetApp; + onInstall(); + expect(SpreadsheetApp.getUi).toHaveBeenCalled(); + }); +}); diff --git a/tests/onInstallonOpen.test.ts b/tests/onInstallonOpen.test.ts deleted file mode 100644 index 4ef62a3..0000000 --- a/tests/onInstallonOpen.test.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { onInstall } from '../src/sheetsl'; - -type MockMenuObj = { - title?: string; - menu: (MockMenuObj | MockMenuItemObj | string)[]; -}; - -type MockMenuItemObj = { - itemName: string; - functionName: string; -}; - -const ADDON_MENU: MockMenuObj = { - menu: [], -}; -const ADDON_SUB_MENU_SETTINGS: MockMenuObj = { - menu: [], -}; -const SEPARATOR = '---'; - -SpreadsheetApp.getUi = jest.fn(() => new MockUi()) as any; - -class MockUi { - isAddonMenu: boolean; - constructor() {} - addItem(itemName: string, functionName: string): this { - if (this.isAddonMenu) { - ADDON_MENU.menu.push({ - itemName: itemName, - functionName: functionName, - }); - } else { - ADDON_SUB_MENU_SETTINGS.menu.push({ - itemName: itemName, - functionName: functionName, - }); - } - return this; - } - addSeparator(): this { - if (this.isAddonMenu) { - ADDON_MENU.menu.push(SEPARATOR); - } else { - ADDON_SUB_MENU_SETTINGS.menu.push(SEPARATOR); - } - return this; - } - addSubMenu(ui: MockUi): this { - ADDON_MENU.menu.push(ADDON_SUB_MENU_SETTINGS); - this.isAddonMenu = true; - return this; - } - addToUi(): void {} - createAddonMenu(): this { - this.isAddonMenu = true; - return this; - } - createMenu(title: string): this { - this.isAddonMenu = false; - ADDON_SUB_MENU_SETTINGS.title = title; - return this; - } -} - -describe('onOpen/onInstall', () => { - test('onOpen/onInstall test', () => { - onInstall(); - expect(ADDON_MENU).toEqual({ - menu: [ - { - title: 'Settings', - menu: [ - { - itemName: 'Set Auth Key', - functionName: 'setDeeplAuthKey', - }, - { - itemName: 'Delete Auth Key', - functionName: 'deleteDeeplAuthKey', - }, - '---', - { - itemName: 'Set Language', - functionName: 'setLanguage', - }, - ], - }, - '---', - { - itemName: 'Translate', - functionName: 'translateRange', - }, - ], - }); - }); -}); diff --git a/tests/setDeeplApiKey.test.ts b/tests/setDeeplApiKey.test.ts new file mode 100644 index 0000000..8103634 --- /dev/null +++ b/tests/setDeeplApiKey.test.ts @@ -0,0 +1,61 @@ +import { setDeeplApiKey } from '../src/sheetsl'; + +describe('setDeeplApiKey', () => { + beforeAll(() => { + global.PropertiesService = { + getUserProperties: jest.fn(() => ({ + setProperty: jest.fn(), + })), + } as unknown as GoogleAppsScript.Properties.PropertiesService; + }); + afterEach(() => { + jest.clearAllMocks(); + }); + it('should successfully save the DeepL API key', () => { + class MockUi { + ButtonSet = { OK_CANCEL: 'ok_cancel' }; + Button = { OK: 'ok' }; + // eslint-disable-next-line @typescript-eslint/no-empty-function + constructor() {} + prompt: jest.Mock = jest.fn( + () => + ({ + getSelectedButton: () => 'ok', + getResponseText: () => 'Sample-API-key:fx', + }) as unknown as GoogleAppsScript.Base.PromptResponse, + ); + alert: jest.Mock = jest.fn(); + } + global.SpreadsheetApp = { + getUi: jest.fn(() => new MockUi() as unknown as GoogleAppsScript.Base.Ui), + } as unknown as GoogleAppsScript.Spreadsheet.SpreadsheetApp; + setDeeplApiKey(); + expect(SpreadsheetApp.getUi).toHaveBeenCalled(); + expect(PropertiesService.getUserProperties).toHaveBeenCalled(); + }); + it('should catch an error if the user cancels the prompt', () => { + class MockUi { + ButtonSet = { OK_CANCEL: 'ok_cancel' }; + Button = { OK: 'ok' }; + // eslint-disable-next-line @typescript-eslint/no-empty-function + constructor() {} + prompt: jest.Mock = jest.fn( + () => + ({ + getSelectedButton: () => 'cancel', + getResponseText: () => null, + }) as unknown as GoogleAppsScript.Base.PromptResponse, + ); + alert: jest.Mock = jest.fn(); + } + global.SpreadsheetApp = { + getUi: jest.fn(() => new MockUi() as unknown as GoogleAppsScript.Base.Ui), + } as unknown as GoogleAppsScript.Spreadsheet.SpreadsheetApp; + // eslint-disable-next-line @typescript-eslint/no-empty-function + jest.spyOn(console, 'error').mockImplementation(() => {}); + setDeeplApiKey(); + expect(SpreadsheetApp.getUi).toHaveBeenCalled(); + expect(PropertiesService.getUserProperties).not.toHaveBeenCalled(); + expect(console.error).toHaveBeenCalled(); + }); +}); diff --git a/tests/verifyAuthKeyPrompt.test.ts b/tests/verifyAuthKeyPrompt.test.ts index fbd7642..a348a19 100644 --- a/tests/verifyAuthKeyPrompt.test.ts +++ b/tests/verifyAuthKeyPrompt.test.ts @@ -1,100 +1,52 @@ -import { verifyAuthKeyPrompt } from '../src/sheetsl'; +import { verifyApiKeyPrompt } from '../src/sheetsl'; -const verifyAuthKeyPromptSuccessPatterns = [ - { - testName: 'Success', - input: { - promptResponse: { - getSelectedButton: () => 'ok', - getResponseText: () => 'ThisIsAnApiAuthKey:fx', - }, - ui: { - Button: { - OK: 'ok', // Don't change this value. - }, - }, - }, - expectedOutput: { +describe('verifyApiKeyPrompt', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + it('should return the original prompt response', () => { + const mockPromptResponse = { getSelectedButton: () => 'ok', - getResponseText: () => 'ThisIsAnApiAuthKey:fx', - }, - }, -] as any[]; - -const verifyAuthKeyPromptErrorPatterns = [ - { - testName: 'Error: canceled auth key setting', - input: { - promptResponse: { - getSelectedButton: () => 'canceled', - getResponseText: () => 'ThisIsAnApiAuthKey:fx', - }, - ui: { - Button: { - OK: 'ok', // Don't change this value. - }, - }, - }, - expectedErrorMessage: - '[SheetsL] Canceled: Setting of DeepL Authentication Key has been canceled.', - }, - { - testName: 'Error: empty auth key', - input: { - promptResponse: { - getSelectedButton: () => 'ok', - getResponseText: () => '', - }, - ui: { - Button: { - OK: 'ok', // Don't change this value. - }, - }, - }, - expectedErrorMessage: - '[SheetsL] You must enter a valid DeepL API Authentication Key.', - }, - { - testName: 'Error: auth key is null', - input: { - promptResponse: { - getSelectedButton: () => 'ok', - getResponseText: () => null, - }, - ui: { - Button: { - OK: 'ok', // Don't change this value. - }, - }, - }, - expectedErrorMessage: - '[SheetsL] You must enter a valid DeepL API Authentication Key.', - }, -] as any[]; - -describe.each(verifyAuthKeyPromptSuccessPatterns)( - 'verifyAuthKeyPrompt Test: success patterns', - ({ testName, input, expectedOutput }) => { - test(`${testName}: getSelectedButton`, () => { - expect( - verifyAuthKeyPrompt(input.promptResponse, input.ui).getSelectedButton(), - ).toBe(expectedOutput.getSelectedButton()); - }); - test(`${testName}: getResponseText`, () => { - expect( - verifyAuthKeyPrompt(input.promptResponse, input.ui).getResponseText(), - ).toBe(expectedOutput.getResponseText()); - }); - }, -); - -describe.each(verifyAuthKeyPromptErrorPatterns)( - 'verifyAuthKeyPrompt Test: error patterns', - ({ testName, input, expectedErrorMessage }) => { - test(testName, () => { - expect(() => { - verifyAuthKeyPrompt(input.promptResponse, input.ui); - }).toThrowError(new Error(expectedErrorMessage)); - }); - }, -); + getResponseText: () => 'Sample-API-key:fx', + } as unknown as GoogleAppsScript.Base.PromptResponse; + const mockUi = { + Button: { + OK: 'ok', + }, + } as unknown as GoogleAppsScript.Base.Ui; + const result = verifyApiKeyPrompt(mockPromptResponse, mockUi); + expect(result).toEqual(mockPromptResponse); + }); + it('should throw an error if the user cancels the prompt', () => { + const mockPromptResponse = { + getSelectedButton: () => 'cancel', + getResponseText: () => 'Sample-API-key:fx', + } as unknown as GoogleAppsScript.Base.PromptResponse; + const mockUi = { + Button: { + OK: 'ok', + }, + } as unknown as GoogleAppsScript.Base.Ui; + expect(() => verifyApiKeyPrompt(mockPromptResponse, mockUi)).toThrow( + new Error( + '[SheetsL] Canceled: Setting of DeepL Authentication Key has been canceled.', + ), + ); + }); + it('should throw an error if the user enters an empty string for the DeepL API key', () => { + const mockPromptResponse = { + getSelectedButton: () => 'ok', + getResponseText: () => '', + } as unknown as GoogleAppsScript.Base.PromptResponse; + const mockUi = { + Button: { + OK: 'ok', + }, + } as unknown as GoogleAppsScript.Base.Ui; + expect(() => verifyApiKeyPrompt(mockPromptResponse, mockUi)).toThrow( + new Error( + '[SheetsL] You must enter a valid DeepL API Authentication Key.', + ), + ); + }); +}); From 32a9c9b6a39b601c94e671fb7dbdd27719113d88 Mon Sep 17 00:00:00 2001 From: ttsukagoshi Date: Wed, 7 Feb 2024 22:42:14 +0900 Subject: [PATCH 6/6] Add `./jest.config.js` to `include` in tsconfig.json --- tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index d52bccb..7cf60fd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -106,5 +106,5 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*.ts", "tests/**/*.ts"] + "include": ["src/**/*.ts", "tests/**/*.ts", "./jest.config.js"] }