Compare commits
2 Commits
85cdfe1443
...
9bc55f3a06
| Author | SHA1 | Date | |
|---|---|---|---|
| 9bc55f3a06 | |||
| 3c6130778e |
@ -38,6 +38,13 @@ The system allows you to:
|
||||
npm run deploy
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run unit tests using Jest:
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
- `src/`: Source code (TypeScript)
|
||||
|
||||
@ -86,3 +86,14 @@ A dedicated side panel provides visibility into the background queue system.
|
||||
- Displays pending edits with timestamps.
|
||||
- Provides controls to globally enable/disable processing.
|
||||
- Allows manual intervention (delete/push) for individual items.
|
||||
|
||||
### 6. Service Layer & Testing
|
||||
|
||||
To enable unit testing without Google Apps Script dependencies, the project uses a Service pattern with Dependency Injection.
|
||||
|
||||
- **`ISpreadsheetService`**: Interface for all sheet interactions.
|
||||
- **`GASSpreadsheetService`**: Production implementation wrapping `SpreadsheetApp`.
|
||||
- **`MockSpreadsheetService`**: In-memory implementation for tests.
|
||||
|
||||
Classes (like `Product`) should accept an `ISpreadsheetService` in their constructor. This allows providing the Mock service during tests to verify logic without touching real Google Sheets.
|
||||
|
||||
|
||||
@ -2,23 +2,24 @@
|
||||
|
||||
## Local Development Environment
|
||||
|
||||
1. **Install Node.js**
|
||||
This project uses `fnm` to manage Node versions.
|
||||
1. **Environment Initialization**
|
||||
Run the provided PowerShell script to automatically install:
|
||||
- `fnm` (Fast Node Manager) via Winget
|
||||
- Node.js (v22)
|
||||
- Global dependencies (`@google/clasp`)
|
||||
- Project dependencies (`npm install`)
|
||||
|
||||
```powershell
|
||||
# Install fnm (if not installed)
|
||||
winget install Schniz.fnm
|
||||
# Configure environment
|
||||
fnm env --use-on-cd | Out-String | Invoke-Expression
|
||||
# Install Node version
|
||||
fnm use --install-if-missing 22
|
||||
.\init.ps1
|
||||
```
|
||||
|
||||
2. **Install Dependencies**
|
||||
2. **Verify Installation**
|
||||
Run tests to confirm the environment is correctly configured.
|
||||
```bash
|
||||
npm install
|
||||
npm install -g @google/clasp
|
||||
npm test
|
||||
```
|
||||
|
||||
|
||||
3. **Clasp Login**
|
||||
Authenticate with Google to allow pushing code.
|
||||
|
||||
|
||||
6
jest.config.js
Normal file
6
jest.config.js
Normal file
@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
testMatch: ['**/*.test.ts'],
|
||||
setupFiles: ['<rootDir>/src/test/setup.ts'],
|
||||
};
|
||||
4734
package-lock.json
generated
4734
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -7,15 +7,20 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "webpack --mode production",
|
||||
"deploy": "clasp push"
|
||||
"deploy": "clasp push",
|
||||
"test": "jest"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/google-apps-script": "^1.0.85",
|
||||
"@types/jest": "^30.0.0",
|
||||
"copy-webpack-plugin": "^13.0.1",
|
||||
"gas-webpack-plugin": "^2.6.0",
|
||||
"graphql-tag": "^2.12.6",
|
||||
"jest": "^29.7.0",
|
||||
"shopify-admin-api-typings": "github:beepmill/shopify-admin-api-typings",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-loader": "^9.5.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"webpack": "^5.96.1",
|
||||
"webpack-cli": "^5.1.4"
|
||||
},
|
||||
|
||||
35
src/Product.test.ts
Normal file
35
src/Product.test.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Product } from "./Product";
|
||||
import { MockSpreadsheetService } from "./services/MockSpreadsheetService";
|
||||
|
||||
describe("Product", () => {
|
||||
let mockService: MockSpreadsheetService;
|
||||
|
||||
beforeEach(() => {
|
||||
// Setup mock data
|
||||
const headers = ["sku", "title", "price", "weight_grams"];
|
||||
const productData = ["TEST-SKU-1", "Test Product", 10.99, 500];
|
||||
|
||||
// product_inventory sheet: Row 1 = headers, Row 2 = data
|
||||
mockService = new MockSpreadsheetService({
|
||||
product_inventory: [
|
||||
headers,
|
||||
productData
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it("should load data from inventory sheet using service", () => {
|
||||
const product = new Product("TEST-SKU-1", mockService);
|
||||
|
||||
expect(product.sku).toBe("TEST-SKU-1");
|
||||
expect(product.title).toBe("Test Product");
|
||||
expect(product.price).toBe(10.99);
|
||||
expect(product.weight_grams).toBe(500);
|
||||
});
|
||||
|
||||
it("should throw error if SKU not found", () => {
|
||||
expect(() => {
|
||||
new Product("NON-EXISTENT-SKU", mockService);
|
||||
}).toThrow("product sku 'NON-EXISTENT-SKU' not found in product_inventory");
|
||||
});
|
||||
});
|
||||
@ -8,16 +8,12 @@ import {
|
||||
ShopifyProductVariant,
|
||||
ShopifyProductSetQuery,
|
||||
ShopifyVariant,
|
||||
VariantOptionValueInput,
|
||||
formatGqlForJSON,
|
||||
} from "./shopifyApi"
|
||||
import * as shopify from "shopify-admin-api-typings"
|
||||
import {
|
||||
getCellRangeByColumnName,
|
||||
getRowByColumnValue,
|
||||
vlookupByColumns,
|
||||
} from "./sheetUtils"
|
||||
import { Config } from "./config"
|
||||
import { ISpreadsheetService } from "./interfaces/ISpreadsheetService"
|
||||
import { GASSpreadsheetService } from "./services/GASSpreadsheetService"
|
||||
|
||||
export class Product {
|
||||
shopify_id: string = ""
|
||||
@ -46,12 +42,15 @@ export class Product {
|
||||
shopify_default_option_value_id: string = ""
|
||||
shopify_status: string = ""
|
||||
|
||||
constructor(sku: string = "") {
|
||||
private sheetService: ISpreadsheetService
|
||||
|
||||
constructor(sku: string = "", sheetService: ISpreadsheetService = new GASSpreadsheetService()) {
|
||||
this.sheetService = sheetService;
|
||||
if (sku == "") {
|
||||
return
|
||||
}
|
||||
this.sku = sku
|
||||
let productRow = getRowByColumnValue("product_inventory", "sku", sku)
|
||||
let productRow = this.sheetService.getRowNumberByColumnValue("product_inventory", "sku", sku)
|
||||
if (productRow == undefined) {
|
||||
throw new Error(
|
||||
"product sku '" + sku + "' not found in product_inventory"
|
||||
@ -61,15 +60,10 @@ export class Product {
|
||||
}
|
||||
|
||||
ImportFromInventory(row: number) {
|
||||
let productInventorySheet =
|
||||
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory")
|
||||
let headers = productInventorySheet
|
||||
.getRange(1, 1, 1, productInventorySheet.getLastColumn())
|
||||
.getValues()[0]
|
||||
let headers = this.sheetService.getHeaders("product_inventory")
|
||||
console.log("headers" + headers)
|
||||
let productValues = productInventorySheet
|
||||
.getRange(row, 1, 1, headers.length)
|
||||
.getValues()[0]
|
||||
let productValues = this.sheetService.getRowData("product_inventory", row)
|
||||
console.log("productValues:" + productValues)
|
||||
console.log("productValues:" + productValues)
|
||||
for (let i = 0; i < headers.length; i++) {
|
||||
if (this.hasOwnProperty(headers[i])) {
|
||||
@ -110,31 +104,26 @@ export class Product {
|
||||
this.shopify_id = this.shopify_product.id.toString()
|
||||
this.shopify_default_variant_id = product.variants.nodes[0].id
|
||||
this.shopify_default_option_id = product.options[0].id
|
||||
this.shopify_default_option_id = product.options[0].id
|
||||
this.shopify_default_option_value_id = product.options[0].optionValues[0].id
|
||||
let productInventorySheet =
|
||||
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory")
|
||||
let row = getRowByColumnValue("product_inventory", "sku", this.sku)
|
||||
getCellRangeByColumnName(productInventorySheet, "shopify_id", row).setValue(
|
||||
this.shopify_id
|
||||
)
|
||||
let row = this.sheetService.getRowNumberByColumnValue("product_inventory", "sku", this.sku)
|
||||
if (row) {
|
||||
this.sheetService.setCellValueByColumnName("product_inventory", row, "shopify_id", this.shopify_id)
|
||||
}
|
||||
}
|
||||
|
||||
ShopifyCategory(): string {
|
||||
return vlookupByColumns(
|
||||
"values",
|
||||
"product_type",
|
||||
this.product_type,
|
||||
"shopify_category"
|
||||
)
|
||||
return this.sheetService.getCellValueByColumnName("values",
|
||||
this.sheetService.getRowNumberByColumnValue("values", "product_type", this.product_type) || 0,
|
||||
"shopify_category"
|
||||
) || ""
|
||||
}
|
||||
|
||||
EbayCategory(): string {
|
||||
return vlookupByColumns(
|
||||
"values",
|
||||
"product_type",
|
||||
this.product_type,
|
||||
"ebay_category_id"
|
||||
)
|
||||
return this.sheetService.getCellValueByColumnName("values",
|
||||
this.sheetService.getRowNumberByColumnValue("values", "product_type", this.product_type) || 0,
|
||||
"ebay_category_id"
|
||||
) || ""
|
||||
}
|
||||
|
||||
ToShopifyProductSet() {
|
||||
@ -188,12 +177,11 @@ export class Product {
|
||||
let response = shop.shopifyGraphQLAPI(query.JSON)
|
||||
let product = response.content.data.productSet.product
|
||||
this.shopify_id = product.id
|
||||
let productInventorySheet =
|
||||
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory")
|
||||
let row = getRowByColumnValue("product_inventory", "sku", this.sku)
|
||||
getCellRangeByColumnName(productInventorySheet, "shopify_id", row).setValue(
|
||||
this.shopify_id
|
||||
)
|
||||
|
||||
let row = this.sheetService.getRowNumberByColumnValue("product_inventory", "sku", this.sku)
|
||||
if (row) {
|
||||
this.sheetService.setCellValueByColumnName("product_inventory", row, "shopify_id", this.shopify_id)
|
||||
}
|
||||
let item: shopify.InventoryItem
|
||||
do {
|
||||
console.log("UpdateShopifyProduct: attempting to get inventory item")
|
||||
@ -335,7 +323,7 @@ export class Product {
|
||||
|
||||
CreatePhotoFolder() {
|
||||
console.log("Product.CreatePhotoFolder()");
|
||||
createPhotoFolderForSku(new(Config), this.sku);
|
||||
createPhotoFolderForSku(new(Config), this.sku, this.sheetService);
|
||||
}
|
||||
|
||||
PublishToShopifyOnlineStore(shop: Shop) {
|
||||
@ -383,7 +371,7 @@ export class Product {
|
||||
}
|
||||
}
|
||||
|
||||
export function createPhotoFolderForSku(config: Config, sku: string) {
|
||||
export function createPhotoFolderForSku(config: Config, sku: string, sheetService: ISpreadsheetService = new GASSpreadsheetService()) {
|
||||
console.log(`createPhotoFolderForSku('${sku}')`)
|
||||
if (!config.productPhotosFolderId) {
|
||||
console.log(
|
||||
@ -392,19 +380,13 @@ export function createPhotoFolderForSku(config: Config, sku: string) {
|
||||
return
|
||||
}
|
||||
|
||||
const productInventorySheet =
|
||||
SpreadsheetApp.getActiveSpreadsheet().getSheetByName("product_inventory")
|
||||
const row = getRowByColumnValue("product_inventory", "sku", sku)
|
||||
const row = sheetService.getRowNumberByColumnValue("product_inventory", "sku", sku)
|
||||
if (!row) {
|
||||
console.log(`SKU '${sku}' not found in sheet. Cannot create folder.`)
|
||||
return
|
||||
}
|
||||
const photosCell = getCellRangeByColumnName(
|
||||
productInventorySheet,
|
||||
"photos",
|
||||
row
|
||||
)
|
||||
const folderUrl = photosCell.getRichTextValue().getLinkUrl()
|
||||
|
||||
const folderUrl = sheetService.getCellHyperlink("product_inventory", row, "photos")
|
||||
console.log(`Folder URL from cell: ${folderUrl}`)
|
||||
|
||||
if (folderUrl && folderUrl.includes("drive.google.com")) {
|
||||
@ -429,9 +411,5 @@ export function createPhotoFolderForSku(config: Config, sku: string) {
|
||||
let url = newFolder.getUrl()
|
||||
console.log(`Folder URL: ${url}`)
|
||||
|
||||
let linkValue = SpreadsheetApp.newRichTextValue()
|
||||
.setText(folderName)
|
||||
.setLinkUrl(url)
|
||||
.build()
|
||||
photosCell.setRichTextValue(linkValue)
|
||||
sheetService.setCellHyperlink("product_inventory", row, "photos", folderName, url)
|
||||
}
|
||||
|
||||
9
src/interfaces/ISpreadsheetService.ts
Normal file
9
src/interfaces/ISpreadsheetService.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export interface ISpreadsheetService {
|
||||
getHeaders(sheetName: string): string[];
|
||||
getRowData(sheetName: string, row: number): any[];
|
||||
getRowNumberByColumnValue(sheetName: string, columnName: string, value: any): number | undefined;
|
||||
setCellValueByColumnName(sheetName: string, row: number, columnName: string, value: any): void;
|
||||
getCellValueByColumnName(sheetName: string, row: number, columnName: string): any;
|
||||
getCellHyperlink(sheetName: string, row: number, columnName: string): string | null;
|
||||
setCellHyperlink(sheetName: string, row: number, columnName: string, displayText: string, url: string): void;
|
||||
}
|
||||
85
src/services/GASSpreadsheetService.ts
Normal file
85
src/services/GASSpreadsheetService.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { ISpreadsheetService } from "../interfaces/ISpreadsheetService";
|
||||
|
||||
export class GASSpreadsheetService implements ISpreadsheetService {
|
||||
private getSheet(sheetName: string): GoogleAppsScript.Spreadsheet.Sheet {
|
||||
const sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName(sheetName);
|
||||
if (!sheet) {
|
||||
throw new Error(`Sheet '${sheetName}' not found`);
|
||||
}
|
||||
return sheet;
|
||||
}
|
||||
|
||||
private getColumnIndex(sheet: GoogleAppsScript.Spreadsheet.Sheet, columnName: string): number {
|
||||
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
|
||||
return headers.indexOf(columnName);
|
||||
}
|
||||
|
||||
getHeaders(sheetName: string): string[] {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
return sheet.getRange(1, 1, 1, sheet.getLastColumn()).getValues()[0];
|
||||
}
|
||||
|
||||
getRowData(sheetName: string, row: number): any[] {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const lastCol = sheet.getLastColumn();
|
||||
// getRange(row, column, numRows, numColumns)
|
||||
return sheet.getRange(row, 1, 1, lastCol).getValues()[0];
|
||||
}
|
||||
|
||||
getRowNumberByColumnValue(sheetName: string, columnName: string, value: any): number | undefined {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||
if (colIndex === -1) return undefined;
|
||||
|
||||
// Get all values in the column. Note: calling getValues() on a large sheet might be slow,
|
||||
// but this matches the previous implementation's performance characteristics more or less.
|
||||
// Ideally we would cache this or use a more efficient find.
|
||||
// offset(1, colIndex) to skip header, but actually getColumnValuesByName usually gets everything including header or handles it.
|
||||
// Original implementation: getColumnValuesByName gets range from row 2.
|
||||
|
||||
const data = sheet.getRange(2, colIndex + 1, sheet.getLastRow() - 1, 1).getValues();
|
||||
const flatData = data.map(r => r[0]);
|
||||
const index = flatData.indexOf(value);
|
||||
|
||||
if (index === -1) return undefined;
|
||||
return index + 2; // +1 for 0-based index, +1 for header row
|
||||
}
|
||||
|
||||
setCellValueByColumnName(sheetName: string, row: number, columnName: string, value: any): void {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||
if (colIndex !== -1) {
|
||||
sheet.getRange(row, colIndex + 1).setValue(value);
|
||||
}
|
||||
}
|
||||
|
||||
getCellValueByColumnName(sheetName: string, row: number, columnName: string): any {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||
if (colIndex !== -1) {
|
||||
return sheet.getRange(row, colIndex + 1).getValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
getCellHyperlink(sheetName: string, row: number, columnName: string): string | null {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||
if (colIndex !== -1) {
|
||||
return sheet.getRange(row, colIndex + 1).getRichTextValue().getLinkUrl();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
setCellHyperlink(sheetName: string, row: number, columnName: string, displayText: string, url: string): void {
|
||||
const sheet = this.getSheet(sheetName);
|
||||
const colIndex = this.getColumnIndex(sheet, columnName);
|
||||
if (colIndex !== -1) {
|
||||
const richText = SpreadsheetApp.newRichTextValue()
|
||||
.setText(displayText)
|
||||
.setLinkUrl(url)
|
||||
.build();
|
||||
sheet.getRange(row, colIndex + 1).setRichTextValue(richText);
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/services/MockSpreadsheetService.ts
Normal file
113
src/services/MockSpreadsheetService.ts
Normal file
@ -0,0 +1,113 @@
|
||||
import { ISpreadsheetService } from "../interfaces/ISpreadsheetService";
|
||||
|
||||
export class MockSpreadsheetService implements ISpreadsheetService {
|
||||
// Store data as a map of sheetName -> array of rows (arrays of values)
|
||||
// Row 0 is headers.
|
||||
private sheets: Map<string, any[][]> = new Map();
|
||||
|
||||
constructor(initialData?: { [sheetName: string]: any[][] }) {
|
||||
if (initialData) {
|
||||
for (const [name, rows] of Object.entries(initialData)) {
|
||||
this.sheets.set(name, JSON.parse(JSON.stringify(rows))); // Deep copy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setSheetData(sheetName: string, data: any[][]) {
|
||||
this.sheets.set(sheetName, data);
|
||||
}
|
||||
|
||||
private getSheet(sheetName: string): any[][] {
|
||||
if (!this.sheets.has(sheetName)) {
|
||||
// Create empty sheet with no headers if accessed but not defined?
|
||||
// Or throw error to mimic GAS?
|
||||
// Let's return empty array or throw.
|
||||
throw new Error(`Sheet '${sheetName}' not found in mock`);
|
||||
}
|
||||
return this.sheets.get(sheetName)!;
|
||||
}
|
||||
|
||||
getHeaders(sheetName: string): string[] {
|
||||
const data = this.getSheet(sheetName);
|
||||
if (data.length === 0) return [];
|
||||
return data[0] as string[];
|
||||
}
|
||||
|
||||
getRowData(sheetName: string, row: number): any[] {
|
||||
const data = this.getSheet(sheetName);
|
||||
// Row is 1-based index
|
||||
if (row > data.length || row < 1) {
|
||||
throw new Error(`Row ${row} out of bounds`);
|
||||
}
|
||||
return data[row - 1]; // Convert to 0-based
|
||||
}
|
||||
|
||||
getRowNumberByColumnValue(sheetName: string, columnName: string, value: any): number | undefined {
|
||||
const data = this.getSheet(sheetName);
|
||||
if (data.length < 2) return undefined; // Only headers or empty
|
||||
|
||||
const headers = data[0];
|
||||
const colIndex = headers.indexOf(columnName);
|
||||
if (colIndex === -1) return undefined;
|
||||
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
if (data[i][colIndex] === value) {
|
||||
return i + 1; // Convert 0-based index to 1-based row number
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
setCellValueByColumnName(sheetName: string, row: number, columnName: string, value: any): void {
|
||||
const data = this.getSheet(sheetName);
|
||||
const headers = data[0];
|
||||
const colIndex = headers.indexOf(columnName);
|
||||
|
||||
if (colIndex === -1) {
|
||||
throw new Error(`Column '${columnName}' not found`);
|
||||
}
|
||||
|
||||
// Ensure row exists, extend if necessary (basic behavior)
|
||||
while (data.length < row) {
|
||||
data.push(new Array(headers.length).fill(""));
|
||||
}
|
||||
|
||||
data[row - 1][colIndex] = value;
|
||||
}
|
||||
|
||||
getCellValueByColumnName(sheetName: string, row: number, columnName: string): any {
|
||||
const data = this.getSheet(sheetName);
|
||||
const headers = data[0];
|
||||
const colIndex = headers.indexOf(columnName);
|
||||
|
||||
if (colIndex === -1) return null;
|
||||
if (row > data.length) return null;
|
||||
|
||||
return data[row - 1][colIndex];
|
||||
}
|
||||
|
||||
// Helper to store links: key = "sheetName:row:colIndex", value = url
|
||||
private links: Map<string, string> = new Map();
|
||||
|
||||
getCellHyperlink(sheetName: string, row: number, columnName: string): string | null {
|
||||
const data = this.getSheet(sheetName);
|
||||
const colIndex = data[0].indexOf(columnName);
|
||||
if (colIndex === -1) return null;
|
||||
|
||||
const key = `${sheetName}:${row}:${colIndex}`;
|
||||
return this.links.get(key) || null;
|
||||
}
|
||||
|
||||
setCellHyperlink(sheetName: string, row: number, columnName: string, displayText: string, url: string): void {
|
||||
// Set text value
|
||||
this.setCellValueByColumnName(sheetName, row, columnName, displayText);
|
||||
|
||||
// Set link
|
||||
const data = this.getSheet(sheetName);
|
||||
const colIndex = data[0].indexOf(columnName);
|
||||
if (colIndex !== -1) {
|
||||
const key = `${sheetName}:${row}:${colIndex}`;
|
||||
this.links.set(key, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/sheetUtils.test.ts
Normal file
5
src/sheetUtils.test.ts
Normal file
@ -0,0 +1,5 @@
|
||||
describe('sheetUtils', () => {
|
||||
it('should be able to run tests', () => {
|
||||
expect(true).toBe(true);
|
||||
});
|
||||
});
|
||||
33
src/test/setup.ts
Normal file
33
src/test/setup.ts
Normal file
@ -0,0 +1,33 @@
|
||||
// Mock global SpreadsheetApp to prevent crashes when importing legacy code
|
||||
global.SpreadsheetApp = {
|
||||
getActive: () => ({
|
||||
getSheetByName: () => ({
|
||||
getDataRange: () => ({
|
||||
offset: () => ({
|
||||
getValues: () => [],
|
||||
clear: () => {},
|
||||
}),
|
||||
getValues: () => [],
|
||||
}),
|
||||
getLastRow: () => 0,
|
||||
getRange: () => ({
|
||||
getValues: () => [],
|
||||
setValue: () => {},
|
||||
setValues: () => {},
|
||||
}),
|
||||
}),
|
||||
getSpreadsheetTimeZone: () => "GMT",
|
||||
}),
|
||||
getActiveSpreadsheet: () => ({
|
||||
getSheetByName: () => null,
|
||||
getSpreadsheetTimeZone: () => "GMT",
|
||||
}),
|
||||
} as any;
|
||||
|
||||
global.Logger = {
|
||||
log: jest.fn(),
|
||||
} as any;
|
||||
|
||||
global.Utilities = {
|
||||
formatDate: () => "2025-01-01",
|
||||
} as any;
|
||||
Reference in New Issue
Block a user