@mcpher/gas-fakes 2.3.17 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +15 -32
  2. package/package.json +1 -2
  3. package/src/cli/app.js +30 -2
  4. package/src/cli/server.js +32 -0
  5. package/src/cli/setup.js +24 -0
  6. package/src/cli/togas.js +176 -0
  7. package/src/index.js +2 -0
  8. package/src/services/common/fakeui.js +45 -0
  9. package/src/services/content/app.js +3 -0
  10. package/src/services/content/contentservice.js +14 -0
  11. package/src/services/content/textoutput.js +45 -0
  12. package/src/services/documentapp/fakedocumentapp.js +1 -1
  13. package/src/services/enums/contentenums.js +15 -0
  14. package/src/services/enums/htmlenums.js +13 -0
  15. package/src/services/enums/scriptenums.js +6 -0
  16. package/src/services/formapp/fakeformapp.js +5 -0
  17. package/src/services/html/app.js +9 -0
  18. package/src/services/html/consumerworker.js +129 -0
  19. package/src/services/html/googlescriptrun.js +91 -0
  20. package/src/services/html/htmloutput.js +127 -0
  21. package/src/services/html/htmloutputmetatag.js +14 -0
  22. package/src/services/html/htmlservice.js +94 -0
  23. package/src/services/html/htmltemplate.js +63 -0
  24. package/src/services/html/serverworker.js +135 -0
  25. package/src/services/html/webapp.js +266 -0
  26. package/src/services/html/worker.js +63 -0
  27. package/src/services/libhandlerapp/fakelibrary.js +2 -2
  28. package/src/services/scriptapp/app.js +44 -0
  29. package/src/services/scriptapp/fakeauthorizationinfo.js +22 -0
  30. package/src/services/slidesapp/fakeslidesapp.js +5 -0
  31. package/src/services/spreadsheetapp/fakebooleancondition.js +14 -2
  32. package/src/services/spreadsheetapp/fakeembeddedchartbuilder.js +30 -5
  33. package/src/services/spreadsheetapp/fakegradientcondition.js +1 -1
  34. package/src/services/spreadsheetapp/fakeovergridimage.js +25 -0
  35. package/src/services/spreadsheetapp/fakesheet.js +23 -1
  36. package/src/services/spreadsheetapp/fakespreadsheet.js +68 -11
  37. package/src/services/spreadsheetapp/fakespreadsheetapp.js +70 -9
  38. package/src/services/stores/fakestores.js +5 -0
  39. package/src/support/auth.js +2 -0
  40. package/src/support/proxies.js +1 -1
  41. package/src/support/sxauth.js +20 -12
  42. package/src/support/utils.js +480 -200
  43. package/src/support/workersync/sxhtml.js +8 -0
  44. package/src/support/workersync/synchronizer.js +8 -1
  45. package/src/support/workersync/worker.js +5 -0
  46. package/api-docs/kdrive_api.json +0 -69958
  47. package/appsscript.json +0 -102
  48. package/gf_agent/README.md +0 -101
  49. package/gf_agent/SKILL.md +0 -460
  50. package/gf_agent/documentation.md +0 -105
  51. package/gf_agent/gf-agent-contributor/SKILL.md +0 -56
  52. package/gf_agent/index.md +0 -21
  53. package/gf_agent/knowledge/00-execution-context.md +0 -5
  54. package/gf_agent/knowledge/01-drive.md +0 -12
  55. package/gf_agent/knowledge/02-syntax.md +0 -14
  56. package/gf_agent/knowledge/03-auth.md +0 -15
  57. package/gf_agent/knowledge/04-advanced.md +0 -46
  58. package/gf_agent/knowledge/05-sheets-forms.md +0 -26
  59. package/gf_agent/knowledge/06-jdbc-cloudsql.md +0 -21
  60. package/gf_agent/knowledge/07-jdbc-auth-details.md +0 -30
  61. package/gf_agent/knowledge/08-docs-limitations.md +0 -4
  62. package/gf_agent/knowledge/09-orchestrator-pattern.md +0 -55
  63. package/gf_agent/knowledge/10-sandbox-security.md +0 -62
  64. package/gf_agent/knowledge/11-chart-builder-limitations.md +0 -15
  65. package/gf_agent/knowledge/12-gmail-eventual-consistency.md +0 -13
  66. package/gf_agent/knowledge/13-advanced-services-discovery.md +0 -23
  67. package/gf_agent/knowledge/14-utilities-parity.md +0 -13
  68. package/gf_agent/knowledge/README.md +0 -16
  69. package/gf_agent/scripts/SKILL.template.md +0 -63
  70. package/gf_agent/scripts/builder.js +0 -118
  71. package/gf_agent/skills/base.md +0 -156
  72. package/gf_agent/skills/cache.md +0 -20
  73. package/gf_agent/skills/calendar.md +0 -780
  74. package/gf_agent/skills/charts.md +0 -127
  75. package/gf_agent/skills/document.md +0 -6752
  76. package/gf_agent/skills/drive.md +0 -423
  77. package/gf_agent/skills/forms.md +0 -4036
  78. package/gf_agent/skills/gmail.md +0 -576
  79. package/gf_agent/skills/jdbc.md +0 -3101
  80. package/gf_agent/skills/lock.md +0 -20
  81. package/gf_agent/skills/properties.md +0 -19
  82. package/gf_agent/skills/script.md +0 -50
  83. package/gf_agent/skills/slides.md +0 -5054
  84. package/gf_agent/skills/spreadsheet.md +0 -56075
  85. package/gf_agent/skills/urlfetch.md +0 -28
  86. package/gf_agent/skills/utilities.md +0 -50
  87. package/gf_agent/skills/xml.md +0 -270
  88. package/skills-lock.json +0 -10
  89. package/src/services/documentapp/fakeui.js +0 -27
@@ -0,0 +1,266 @@
1
+ import http from 'http';
2
+ import { ServerWorkerContext } from './serverworker.js';
3
+
4
+ const CLIENT_POLYFILL = `
5
+ <link rel="stylesheet" href="https://ssl.gstatic.com/docs/script/css/add-ons1.css">
6
+ <script>
7
+ window.google = window.google || {};
8
+ window.google.script = window.google.script || {};
9
+ window.google.script.run = (function() {
10
+ function RunProxy(handlers) {
11
+ this._sh = handlers ? handlers._sh : null;
12
+ this._fh = handlers ? handlers._fh : null;
13
+ this._uo = handlers ? handlers._uo : null;
14
+
15
+ return new Proxy(this, {
16
+ get: (target, prop) => {
17
+ if (prop === 'withSuccessHandler') return (h) => new RunProxy({ ...target, _sh: h });
18
+ if (prop === 'withFailureHandler') return (h) => new RunProxy({ ...target, _fh: h });
19
+ if (prop === 'withUserObject') return (o) => new RunProxy({ ...target, _uo: o });
20
+
21
+ return (...args) => {
22
+ let hasError = false;
23
+ let errorMsg = '';
24
+
25
+ // Validate arguments according to Google Apps Script rules
26
+ const validateArg = (arg) => {
27
+ if (arg === undefined) {
28
+ hasError = true;
29
+ errorMsg = 'google.script.run cannot process undefined arguments.';
30
+ } else if (typeof arg === 'function') {
31
+ hasError = true;
32
+ errorMsg = 'google.script.run cannot process function arguments.';
33
+ } else if (typeof Element !== 'undefined' && arg instanceof Element) {
34
+ hasError = true;
35
+ errorMsg = 'google.script.run cannot process DOM element arguments.';
36
+ } else if (arg === window) {
37
+ hasError = true;
38
+ errorMsg = 'google.script.run cannot process window object arguments.';
39
+ } else if (arg !== null && typeof arg === 'object') {
40
+ // deep validation could go here, but shallow is usually enough for the common mistakes
41
+ }
42
+ };
43
+ args.forEach(validateArg);
44
+
45
+ if (hasError) {
46
+ if (target._fh) {
47
+ setTimeout(() => target._fh(new Error(errorMsg), target._uo), 0);
48
+ }
49
+ return;
50
+ }
51
+
52
+ fetch(window.location.origin + '/__gas_rpc', {
53
+ method: 'POST',
54
+ headers: { 'Content-Type': 'application/json' },
55
+ body: JSON.stringify({ funcName: prop, args: args })
56
+ })
57
+ .then(r => r.json())
58
+ .then(data => {
59
+ if (data.error) {
60
+ if (target._fh) target._fh(new Error(data.error), target._uo);
61
+ } else {
62
+ if (target._sh) target._sh(data.result, target._uo);
63
+ }
64
+ })
65
+ .catch(err => {
66
+ if (target._fh) target._fh(err, target._uo);
67
+ });
68
+ };
69
+ }
70
+ });
71
+ }
72
+ return new RunProxy();
73
+ })();
74
+ window.google.script.host = {
75
+ close: function() {
76
+ if (window.top && window.top !== window) {
77
+ window.top.postMessage({ type: 'gas-fakes-close' }, '*');
78
+ } else {
79
+ document.body.innerHTML = '<h3 style="font-family: sans-serif; text-align: center; margin-top: 50px;">Dialog Closed. You may close this tab.</h3>';
80
+ }
81
+ },
82
+ origin: window.location.origin,
83
+ setHeight: function(h) { console.log('gas-fakes: Host setHeight called with', h); },
84
+ setWidth: function(w) { console.log('gas-fakes: Host setWidth called with', w); }
85
+ };
86
+ //# sourceURL=gas-fakes:///polyfill.js
87
+ </script>
88
+ `;
89
+
90
+ export function startServer(port = 3000, scriptPath = null, entryFunction = 'doGet') {
91
+ const server = http.createServer((req, res) => {
92
+ if (req.method === 'GET' && req.url.startsWith('/')) {
93
+ // 1. Serve GET request
94
+ try {
95
+ const ctx = new ServerWorkerContext(scriptPath);
96
+ // Emulate the event object passed to doGet
97
+ const url = new URL(req.url, `http://${req.headers.host}`);
98
+ const params = Object.fromEntries(url.searchParams.entries());
99
+ const getEvent = {
100
+ parameter: params,
101
+ parameters: Object.fromEntries([...url.searchParams.keys()].map(k => [k, url.searchParams.getAll(k)])),
102
+ queryString: url.search.substring(1),
103
+ contextPath: '',
104
+ contentLength: 0
105
+ };
106
+ const funcToRun = params.main || entryFunction || 'doGet';
107
+ const result = ctx.runFunction(funcToRun, [getEvent]);
108
+
109
+ if (result && result.__isHtmlOutput) {
110
+ let html = result.content;
111
+ // Inject polyfill
112
+ if (html.toLowerCase().includes('</head>')) {
113
+ html = html.replace(/<\/head>/i, CLIENT_POLYFILL + '</head>');
114
+ } else {
115
+ html = CLIENT_POLYFILL + html;
116
+ }
117
+
118
+ if (result.__framingType === 'modal' || result.__framingType === 'sidebar') {
119
+ const isSidebar = result.__framingType === 'sidebar';
120
+ const widthStr = isSidebar ? '300px' : '600px';
121
+ const heightStr = isSidebar ? '100vh' : '450px';
122
+ const safeTitle = (result.title || 'gas-fakes Dialog').replace(/"/g, '&quot;');
123
+
124
+ const modalCss = `
125
+ body { background-color: rgba(0,0,0,0.6); margin: 0; display: flex; align-items: center; justify-content: center; height: 100vh; font-family: 'Google Sans', Roboto, Arial, sans-serif; overflow: hidden; }
126
+ .dialog { width: ${widthStr}; height: ${heightStr}; background: #fff; border-radius: 8px; box-shadow: 0 24px 38px 3px rgba(0,0,0,0.14), 0 9px 46px 8px rgba(0,0,0,0.12), 0 11px 15px -7px rgba(0,0,0,0.2); display: flex; flex-direction: column; overflow: hidden; }
127
+ .dialog-header { display: flex; justify-content: space-between; align-items: center; padding: 20px 24px 14px 24px; border-bottom: 1px solid transparent; }
128
+ .dialog-title { font-size: 22px; color: #202124; margin: 0; line-height: 28px; font-weight: 400; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
129
+ .dialog-close { background: none; border: none; font-size: 20px; color: #5f6368; cursor: pointer; padding: 0; line-height: 1; }
130
+ .dialog-close:hover { color: #202124; }
131
+ .dialog-content { padding: 0; flex-grow: 1; display: flex; border: none; }
132
+ iframe { border: none; width: 100%; height: 100%; background: #fff; }
133
+ `;
134
+
135
+ const sidebarCss = `
136
+ body { background-color: transparent; margin: 0; display: flex; justify-content: flex-end; height: 100vh; font-family: 'Google Sans', Roboto, Arial, sans-serif; overflow: hidden; pointer-events: none; }
137
+ .dialog { width: ${widthStr}; height: ${heightStr}; background: #fff; box-shadow: -1px 0 4px rgba(0,0,0,0.2); display: flex; flex-direction: column; pointer-events: auto; border-left: 1px solid #dadce0; }
138
+ .dialog-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid #dadce0; }
139
+ .dialog-title { font-size: 16px; color: #202124; margin: 0; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
140
+ .dialog-close { background: none; border: none; font-size: 20px; color: #5f6368; cursor: pointer; padding: 0; line-height: 1; }
141
+ .dialog-close:hover { color: #202124; }
142
+ .dialog-content { flex-grow: 1; display: flex; border: none; }
143
+ iframe { border: none; width: 100%; height: 100%; background: #fff; }
144
+ `;
145
+
146
+ const css = isSidebar ? sidebarCss : modalCss;
147
+
148
+ // We encode the HTML so we can safely inject it into the iframe using document.write
149
+ const encodedHtml = encodeURIComponent(html);
150
+
151
+ html = `
152
+ <!DOCTYPE html>
153
+ <html>
154
+ <head>
155
+ <title>${safeTitle}</title>
156
+ <style>${css}</style>
157
+ </head>
158
+ <body>
159
+ <div class="dialog">
160
+ <div class="dialog-header">
161
+ <h2 class="dialog-title">${safeTitle}</h2>
162
+ <button class="dialog-close" aria-label="Close" onclick="closeDialog()">x</button>
163
+ </div>
164
+ <div class="dialog-content">
165
+ <iframe id="gas-fakes-frame"></iframe>
166
+ </div>
167
+ </div>
168
+ <script>
169
+ // Handle programmatic closures from google.script.host.close()
170
+ window.addEventListener('message', function(e) {
171
+ if (e.data && e.data.type === 'gas-fakes-close') {
172
+ document.body.innerHTML = '<h3 style="font-family: sans-serif; text-align: center; margin-top: 50px;">Dialog Closed. You may close this tab.</h3>';
173
+ }
174
+ });
175
+ // Handle the 'X' button
176
+ function closeDialog() {
177
+ document.body.innerHTML = '<h3 style="font-family: sans-serif; text-align: center; margin-top: 50px;">Dialog Closed. You may close this tab.</h3>';
178
+ }
179
+
180
+ const iframe = document.getElementById('gas-fakes-frame');
181
+ iframe.contentWindow.document.open();
182
+ iframe.contentWindow.document.write(decodeURIComponent("${encodedHtml}"));
183
+ iframe.contentWindow.document.close();
184
+ </script>
185
+ </body>
186
+ </html>`;
187
+ }
188
+
189
+ res.writeHead(200, { 'Content-Type': 'text/html' }); res.end(html);
190
+ } else if (result && result.__isTextOutput) {
191
+ res.writeHead(200, { 'Content-Type': result.mimeType || 'text/plain' });
192
+ res.end(result.content);
193
+ } else {
194
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
195
+ res.end(typeof result === 'string' ? result : JSON.stringify(result));
196
+ }
197
+ } catch (err) {
198
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
199
+ res.end("Error executing " + entryFunction + ": " + err.message);
200
+ }
201
+ } else if (req.method === 'POST' && req.url === '/__gas_rpc') {
202
+ // 2. Handle google.script.run RPC requests
203
+ let body = '';
204
+ req.on('data', chunk => body += chunk.toString());
205
+ req.on('end', () => {
206
+ try {
207
+ const payload = JSON.parse(body);
208
+ const ctx = new ServerWorkerContext(scriptPath);
209
+ // Execute the requested function statelessly
210
+ const result = ctx.runFunction(payload.funcName, payload.args);
211
+ res.writeHead(200, { 'Content-Type': 'application/json' });
212
+ res.end(JSON.stringify({ result }));
213
+ } catch (err) {
214
+ res.writeHead(200, { 'Content-Type': 'application/json' });
215
+ res.end(JSON.stringify({ error: err.message }));
216
+ }
217
+ });
218
+ } else if (req.method === 'POST' && req.url === '/') {
219
+ // 3. Serve doPost
220
+ let body = '';
221
+ req.on('data', chunk => body += chunk.toString());
222
+ req.on('end', () => {
223
+ try {
224
+ const ctx = new ServerWorkerContext(scriptPath);
225
+ const doPostEvent = {
226
+ postData: {
227
+ contents: body,
228
+ type: req.headers['content-type']
229
+ },
230
+ parameter: {},
231
+ parameters: {},
232
+ queryString: '',
233
+ contextPath: '',
234
+ contentLength: body.length
235
+ };
236
+ const result = ctx.runFunction('doPost', [doPostEvent]);
237
+
238
+ if (result && result.__isHtmlOutput) {
239
+ res.writeHead(200, { 'Content-Type': 'text/html' });
240
+ res.end(result.content);
241
+ } else if (result && result.__isTextOutput) {
242
+ res.writeHead(200, { 'Content-Type': result.mimeType || 'text/plain' });
243
+ res.end(result.content);
244
+ } else {
245
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
246
+ res.end(typeof result === 'string' ? result : JSON.stringify(result));
247
+ }
248
+ } catch (err) {
249
+ res.writeHead(500, { 'Content-Type': 'text/plain' });
250
+ res.end("Error executing doPost: " + err.message);
251
+ }
252
+ });
253
+ } else {
254
+ res.writeHead(404);
255
+ res.end('Not found');
256
+ }
257
+ });
258
+
259
+ server.listen(port, () => {
260
+ console.log('\n=================================================');
261
+ console.log('🚀 gas-fakes Web App running at: http://localhost:' + port);
262
+ console.log('=================================================\n');
263
+ });
264
+
265
+ return server;
266
+ }
@@ -0,0 +1,63 @@
1
+ import { parentPort, workerData } from 'worker_threads';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import { runInThisContext } from 'vm';
5
+ import { Utils as GasFakesUtils } from '../../support/utils.js';
6
+
7
+ // Initialize the gas-fakes environment in this worker isolate
8
+ import '../../index.js';
9
+
10
+ // The worker is spawned with the path to the main script
11
+ const { mainScriptPath, funcName, args, isTemplate, templateString } = workerData;
12
+
13
+ async function run() {
14
+ try {
15
+ const projectDir = path.dirname(mainScriptPath);
16
+
17
+ // Simulate Apps Script's global execution model:
18
+ // Read all .js and .gs files in the directory and evaluate them in this global context.
19
+ const files = fs.readdirSync(projectDir);
20
+ const scriptFiles = files.filter(f => f.endsWith('.js') || f.endsWith('.gs'));
21
+
22
+ // Sort files to ensure deterministic execution (like clasp does)
23
+ scriptFiles.sort();
24
+
25
+ for (const file of scriptFiles) {
26
+ const fullPath = path.join(projectDir, file);
27
+ const content = fs.readFileSync(fullPath, 'utf8');
28
+
29
+ // Strip ESM keywords to allow evaluation in the raw VM context
30
+ const strippedContent = GasFakesUtils.stripEsmKeywords(content);
31
+
32
+ try {
33
+ // runInThisContext executes the code globally within the current V8 isolate
34
+ runInThisContext(strippedContent, { filename: fullPath });
35
+ } catch (e) {
36
+ console.error(`gas-fakes Error parsing ${file}: ${e.message}`);
37
+ }
38
+ }
39
+
40
+ if (isTemplate) {
41
+ parentPort.postMessage({ result: "Template evaluation not yet implemented in worker" });
42
+ return;
43
+ }
44
+
45
+ const func = globalThis[funcName];
46
+
47
+ if (typeof func !== 'function') {
48
+ throw new Error(`google.script.run: function "${funcName}" is not defined on the server.`);
49
+ }
50
+
51
+ // Apps Script functions can be async, so we await
52
+ const result = await func(...args);
53
+
54
+ // Apps Script parameters/returns are JSON serialized
55
+ const serializedResult = typeof result === 'undefined' ? undefined : JSON.parse(JSON.stringify(result));
56
+
57
+ parentPort.postMessage({ result: serializedResult });
58
+ } catch (error) {
59
+ parentPort.postMessage({ error: { message: error.message, stack: error.stack } });
60
+ }
61
+ }
62
+
63
+ run();
@@ -60,7 +60,7 @@ class FakeLibrary {
60
60
  }
61
61
 
62
62
  get combinedJs() {
63
- return this.libContent.files.filter((f) => f.type === 'server_js').map((f) => `////-- ${f.name} --\n${f.source}`).join(`\n\n`)
63
+ return this.libContent.files.filter((f) => f.type === 'server_js').map((f) => `////-- ${f.name} --\n${f.source}\n;`).join(`\n\n`)
64
64
  }
65
65
 
66
66
  get content() {
@@ -90,7 +90,7 @@ class FakeLibrary {
90
90
  const functions = makeExports(ast, 'FunctionDeclaration', (f) => f.id.name)
91
91
  const variables = makeExports(ast, 'VariableDeclaration', (f) => f.declarations[0].id.name)
92
92
  const classes = makeExports(ast, 'ClassDeclaration', (f) => f.id.name)
93
- const exports = [...functions, ...variables, ...classes];
93
+ const exports = [...functions, ...variables, ...classes].filter(Boolean);
94
94
  return `${this.combinedJs};\nreturn { ${exports.join(', ')} };`;
95
95
  }
96
96
 
@@ -6,8 +6,12 @@ import { Syncit } from '../../support/syncit.js'
6
6
  import { Auth } from '../../support/auth.js'
7
7
  import { Proxies } from '../../support/proxies.js'
8
8
  import { newFakeBehavior } from './behavior.js'
9
+ import { newFakeAuthorizationInfo } from './fakeauthorizationinfo.js'
10
+ import { ScriptEnums } from '../enums/scriptenums.js'
9
11
  import { newCacheDropin } from '@mcpher/gas-flex-cache'
10
12
  import { slogger } from "../../support/slogger.js";
13
+ import fs from 'fs';
14
+ import path from 'path';
11
15
 
12
16
  // This will eventually hold a proxy for ScriptApp
13
17
  let _app = null
@@ -155,6 +159,41 @@ if (typeof globalThis[name] === typeof undefined) {
155
159
  _app = {
156
160
  getOAuthToken,
157
161
  __getSourceOAuthToken: getSourceOAuthToken,
162
+ getResource: (filename) => {
163
+ let mainScriptPath = process.argv[1];
164
+ if (globalThis.__gasFakesMainScriptPath) {
165
+ mainScriptPath = globalThis.__gasFakesMainScriptPath;
166
+ }
167
+ if (!mainScriptPath || mainScriptPath.endsWith('node')) {
168
+ throw new Error("Could not determine project root for getResource. Ensure process.argv[1] is set or __gasFakesMainScriptPath is defined.");
169
+ }
170
+
171
+ const projectDir = path.dirname(mainScriptPath);
172
+ let fullPath = path.resolve(projectDir, filename);
173
+
174
+ if (!fs.existsSync(fullPath)) {
175
+ // Apps Script allows omitting the extension. Try common ones.
176
+ const exts = ['.js', '.gs', '.html'];
177
+ let found = false;
178
+ for (const ext of exts) {
179
+ if (fs.existsSync(fullPath + ext)) {
180
+ fullPath += ext;
181
+ found = true;
182
+ break;
183
+ }
184
+ }
185
+ if (!found) {
186
+ throw new Error(`File not found: ${fullPath}`);
187
+ }
188
+ }
189
+ const content = fs.readFileSync(fullPath, 'utf8');
190
+ // We can't import Utilities easily here without circular deps or breaking the proxy
191
+ // But we can just use the global Utilities object assuming it's loaded.
192
+ if (!globalThis.Utilities) {
193
+ throw new Error("Utilities service not loaded, cannot create Blob for getResource.");
194
+ }
195
+ return globalThis.Utilities.newBlob(content, "text/plain", filename);
196
+ },
158
197
  requireAllScopes,
159
198
  requireScopes,
160
199
  getScriptId: () => {
@@ -198,6 +237,11 @@ if (typeof globalThis[name] === typeof undefined) {
198
237
  AuthMode: {
199
238
  FULL: 'FULL'
200
239
  },
240
+ AuthorizationStatus: ScriptEnums.AuthorizationStatus,
241
+ getAuthorizationInfo: (authMode) => {
242
+ ensureInit();
243
+ return newFakeAuthorizationInfo(authMode);
244
+ },
201
245
  // __behavior added below to break recursion
202
246
  __newCacheDropin: newCacheDropin,
203
247
  __proxies: Proxies,
@@ -0,0 +1,22 @@
1
+ import { ScriptEnums } from '../enums/scriptenums.js';
2
+
3
+ export class FakeAuthorizationInfo {
4
+ constructor(authMode) {
5
+ this._authMode = authMode;
6
+ }
7
+
8
+ getAuthorizationStatus() {
9
+ // gas-fakes always handles auth out-of-band via CLI,
10
+ // so during script execution, we assume auth is not required.
11
+ return ScriptEnums.AuthorizationStatus.NOT_REQUIRED;
12
+ }
13
+
14
+ getAuthorizationUrl() {
15
+ // Return null since we don't require inline authorization
16
+ return null;
17
+ }
18
+ }
19
+
20
+ export const newFakeAuthorizationInfo = (authMode) => {
21
+ return new FakeAuthorizationInfo(authMode);
22
+ };
@@ -1,5 +1,6 @@
1
1
  import { Proxies } from '../../support/proxies.js';
2
2
  import { newFakeAffineTransformBuilder } from './fakeaffinetransformbuilder.js';
3
+ import { newFakeUi } from '../common/fakeui.js';
3
4
  import * as Enums from '../enums/slidesenums.js';
4
5
  import { newFakePresentation } from './fakepresentation.js';
5
6
  import { Auth } from '../../support/auth.js';
@@ -85,6 +86,10 @@ class FakeSlidesApp {
85
86
  return this.openById(id);
86
87
  }
87
88
 
89
+ getUi() {
90
+ return newFakeUi();
91
+ }
92
+
88
93
  /**
89
94
  * Opens the presentation with the specified ID.
90
95
  * @param {string} id The ID of the presentation to open.
@@ -83,8 +83,20 @@ export class FakeBooleanCondition {
83
83
  if (!this.__apiCondition.values) return [];
84
84
 
85
85
  // API condition values are typically { userEnteredValue: 'string' } or { relativeDate: 'TODAY' }
86
+ const type = this.getCriteriaType()?.toString() || '';
87
+ const isNumberType = type.startsWith('NUMBER_');
88
+ const isDateType = type.startsWith('DATE_') && !type.endsWith('_RELATIVE');
89
+
86
90
  return this.__apiCondition.values.map(v => {
87
- if (v.userEnteredValue !== undefined) return v.userEnteredValue;
91
+ if (v.userEnteredValue !== undefined) {
92
+ if (isNumberType && !isNaN(v.userEnteredValue) && v.userEnteredValue !== '') {
93
+ return Number(v.userEnteredValue);
94
+ }
95
+ if (isDateType && !isNaN(Date.parse(v.userEnteredValue))) {
96
+ return new Date(v.userEnteredValue);
97
+ }
98
+ return v.userEnteredValue;
99
+ }
88
100
  // In Apps Script, RelativeDate enum values are typically returned as strings matching the enum
89
101
  if (v.relativeDate !== undefined) return SpreadsheetApp.RelativeDate[v.relativeDate] || v.relativeDate;
90
102
  return null;
@@ -114,6 +126,6 @@ export class FakeBooleanCondition {
114
126
  }
115
127
 
116
128
  toString() {
117
- return 'BooleanCondition';
129
+ return 'ConditionalFormatBooleanCondition';
118
130
  }
119
131
  }
@@ -59,15 +59,40 @@ export class FakeEmbeddedChartBuilder {
59
59
  if (nargs !== 1) matchThrow();
60
60
 
61
61
  const gridRange = makeSheetsGridRange(range);
62
+ const startCol = gridRange.startColumnIndex || 0;
63
+ const endCol = gridRange.endColumnIndex || startCol + 1;
64
+ const numCols = endCol - startCol;
62
65
 
63
- if (this.__apiChart.spec.basicChart.domains.length === 0) {
66
+ // If it's a multi-column range, live GAS splits the first column as domain
67
+ // and the rest as series (unless it's already got a domain)
68
+ if (numCols > 1 && this.__apiChart.spec.basicChart.domains.length === 0) {
69
+ // First column is domain
70
+ const domainRange = JSON.parse(JSON.stringify(gridRange));
71
+ domainRange.endColumnIndex = startCol + 1;
64
72
  this.__apiChart.spec.basicChart.domains.push({
65
- domain: { sourceRange: { sources: [gridRange] } },
73
+ domain: { sourceRange: { sources: [domainRange] } },
66
74
  });
75
+
76
+ // Rest are series
77
+ for (let i = 1; i < numCols; i++) {
78
+ const seriesRange = JSON.parse(JSON.stringify(gridRange));
79
+ seriesRange.startColumnIndex = startCol + i;
80
+ seriesRange.endColumnIndex = startCol + i + 1;
81
+ this.__apiChart.spec.basicChart.series.push({
82
+ series: { sourceRange: { sources: [seriesRange] } },
83
+ });
84
+ }
67
85
  } else {
68
- this.__apiChart.spec.basicChart.series.push({
69
- series: { sourceRange: { sources: [gridRange] } },
70
- });
86
+ // Standard behavior
87
+ if (this.__apiChart.spec.basicChart.domains.length === 0) {
88
+ this.__apiChart.spec.basicChart.domains.push({
89
+ domain: { sourceRange: { sources: [gridRange] } },
90
+ });
91
+ } else {
92
+ this.__apiChart.spec.basicChart.series.push({
93
+ series: { sourceRange: { sources: [gridRange] } },
94
+ });
95
+ }
71
96
  }
72
97
  return this;
73
98
  }
@@ -66,6 +66,6 @@ export class FakeGradientCondition {
66
66
  }
67
67
 
68
68
  toString() {
69
- return 'GradientCondition';
69
+ return 'ConditionalFormatGradientCondition';
70
70
  }
71
71
  }
@@ -44,4 +44,29 @@ export class FakeOverGridImage {
44
44
  getHeight() {
45
45
  return this.object.height;
46
46
  }
47
+
48
+ setHeight(height) {
49
+ this.object.height = height;
50
+ return this;
51
+ }
52
+
53
+ setWidth(width) {
54
+ this.object.width = width;
55
+ return this;
56
+ }
57
+
58
+ setAltTextTitle(title) {
59
+ this.object.altTextTitle = title;
60
+ return this;
61
+ }
62
+
63
+ setAltTextDescription(description) {
64
+ this.object.altTextDescription = description;
65
+ return this;
66
+ }
67
+
68
+ deleteOverlaidDrawing() {
69
+ // No-op for now as it's a mock
70
+ return this;
71
+ }
47
72
  }
@@ -38,7 +38,6 @@ export class FakeSheet {
38
38
 
39
39
  const props = [
40
40
  // "getImages",
41
- "insertImage",
42
41
  "removeImage",
43
42
  // "getNamedRanges",
44
43
  // "getRangeByName", // <--- This is a method for Class Spreadsheet.
@@ -1184,6 +1183,29 @@ export class FakeSheet {
1184
1183
  return [];
1185
1184
  }
1186
1185
 
1186
+ insertImage(blobSourceOrUrl, column, row, offsetX, offsetY) {
1187
+ const { nargs, matchThrow } = signatureArgs(arguments, "Sheet.insertImage");
1188
+ if (nargs < 3 || nargs > 5) matchThrow();
1189
+
1190
+ // Warn about API limitations
1191
+ console.warn(
1192
+ "Sheet.insertImage: Floating images are not supported via the Google Sheets REST API v4. This is a local mock."
1193
+ );
1194
+
1195
+ const obj = {
1196
+ row: row - 1,
1197
+ col: column - 1,
1198
+ anchorCellXOffset: offsetX || 0,
1199
+ anchorCellYOffset: offsetY || 0,
1200
+ width: 100, // Default width
1201
+ height: 100, // Default height
1202
+ blob: typeof blobSourceOrUrl === "string" ? null : blobSourceOrUrl,
1203
+ url: typeof blobSourceOrUrl === "string" ? blobSourceOrUrl : null,
1204
+ };
1205
+
1206
+ return newFakeOverGridImage(this, obj);
1207
+ }
1208
+
1187
1209
  getImages() {
1188
1210
  const url = `https://docs.google.com/spreadsheets/export?exportFormat=xlsx&id=${this.__parent.__meta.spreadsheetId}`;
1189
1211
  const res = UrlFetchApp.fetch(url, {