@mcpher/gas-fakes 1.0.17 → 1.0.19
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.
- package/.aiexclude +6 -0
- package/gasmess/bruce/pbx.js +58 -8
- package/gasmess/bruce/sorting.js +106 -0
- package/gasmess/tanaike/for_simple_test.js +3 -8
- package/gasmess/tanaike/sample5.js +63 -0
- package/ghissues/image-size-inconsistency-issue.sh +46 -0
- package/ghissues/issue-formapp-create-title-inconsistency.sh +51 -0
- package/ghissues/protection-editors-issue.sh +33 -0
- package/ghissues/review-sandbox-listing-issue.sh +45 -0
- package/package.json +5 -2
- package/regenerate-progress-reports.sh +38 -0
- package/src/index.js +3 -1
- package/src/services/advdocs/fakeadvdocs.js +6 -1
- package/src/services/advdocs/fakeadvdocuments.js +7 -16
- package/src/services/advdrive/fakeadvdrivefiles.js +13 -3
- package/src/services/advdrive/fakeadvdrivepermissions.js +6 -3
- package/src/services/advforms/app.js +31 -0
- package/src/services/advforms/fakeadvforms.js +221 -0
- package/src/services/advforms/fakeadvformsform.js +80 -0
- package/src/services/advforms/formsapis.js +15 -0
- package/src/services/advsheets/fakeadvsheets.js +0 -7
- package/src/services/advsheets/fakeadvsheetsdevelopermetadata.js +4 -2
- package/src/services/advsheets/fakeadvsheetsspreadsheets.js +18 -5
- package/src/services/advsheets/fakeadvsheetsvalues.js +4 -2
- package/src/services/advslides/fakeadvslides.js +10 -12
- package/src/services/documentapp/app.js +25 -3
- package/src/services/documentapp/appenderhelpers.js +32 -2
- package/src/services/documentapp/elementblasters.js +78 -2
- package/src/services/documentapp/elementhelpers.js +106 -1
- package/src/services/documentapp/elementoptions.js +98 -17
- package/src/services/documentapp/fakebody.js +244 -1
- package/src/services/documentapp/fakedocument.js +55 -4
- package/src/services/documentapp/fakedocumentapp.js +23 -6
- package/src/services/documentapp/fakelistitem.js +72 -1
- package/src/services/documentapp/fakeparagraph.js +101 -63
- package/src/services/documentapp/shadowdocument.js +27 -33
- package/src/services/documentapp/stylehelpers.js +1 -0
- package/src/services/driveapp/fakedriveapp.js +0 -6
- package/src/services/driveapp/fakedrivefile.js +2 -3
- package/src/services/driveapp/fakefolderapp.js +1 -1
- package/src/services/enums/docsenums.js +2 -1
- package/src/services/enums/formsenums.js +62 -0
- package/src/services/formapp/app.js +44 -0
- package/src/services/formapp/fakecheckboxitem.js +142 -0
- package/src/services/formapp/fakechoice.js +45 -0
- package/src/services/formapp/fakeform.js +167 -0
- package/src/services/formapp/fakeformapp.js +107 -0
- package/src/services/formapp/fakeformitem.js +218 -0
- package/src/services/formapp/formitemregistry.js +23 -0
- package/src/services/formapp/formitems.js +6 -0
- package/src/services/scriptapp/app.js +2 -1
- package/src/services/scriptapp/behavior.js +291 -11
- package/src/services/slidesapp/app.js +25 -4
- package/src/services/slidesapp/fakepresentation.js +3 -1
- package/src/services/slidesapp/fakeslidesapp.js +5 -1
- package/src/services/spreadsheetapp/app.js +26 -9
- package/src/services/spreadsheetapp/fakeprotection.js +448 -0
- package/src/services/spreadsheetapp/fakesheet.js +63 -9
- package/src/services/spreadsheetapp/fakesheetrange.js +25 -1
- package/src/services/spreadsheetapp/fakespreadsheet.js +41 -2
- package/src/services/spreadsheetapp/fakespreadsheetapp.js +9 -1
- package/src/support/formscacher.js +7 -0
- package/src/support/helpers.js +1 -3
- package/src/support/proxies.js +14 -3
- package/src/support/sxdocs.js +3 -3
- package/src/support/sxforms.js +50 -0
- package/src/support/syncit.js +4 -0
- package/src/support/workersync/sxfunctions.js +1 -0
- /package/{fakepositionedimage.js → fakeadvdocuments.js} +0 -0
- /package/src/services/{session → base}/app.js +0 -0
- /package/src/services/{session → base}/fakesession.js +0 -0
package/.aiexclude
ADDED
package/gasmess/bruce/pbx.js
CHANGED
|
@@ -2,8 +2,58 @@ import '../../main.js';
|
|
|
2
2
|
import { moveToTempFolder, deleteTempFile } from '../tempfolder.js';
|
|
3
3
|
import { report, scl } from './dreport.js';
|
|
4
4
|
|
|
5
|
+
|
|
6
|
+
|
|
5
7
|
const suffix = "-bruce"
|
|
6
8
|
|
|
9
|
+
const tx2 = () => {
|
|
10
|
+
let doc = DocumentApp.create("abc")
|
|
11
|
+
const id = doc.getId()
|
|
12
|
+
moveToTempFolder(id, suffix)
|
|
13
|
+
|
|
14
|
+
let body = doc.getBody()
|
|
15
|
+
|
|
16
|
+
const p1 = body.appendParagraph("p1")
|
|
17
|
+
doc = scl(doc)
|
|
18
|
+
let d = Docs.Documents.get(id)
|
|
19
|
+
console.log(JSON.stringify(d))
|
|
20
|
+
|
|
21
|
+
const attributesToSet = {
|
|
22
|
+
[DocumentApp.Attribute.HORIZONTAL_ALIGNMENT]: DocumentApp.HorizontalAlignment.CENTER,
|
|
23
|
+
[DocumentApp.Attribute.ITALIC]: true,
|
|
24
|
+
[DocumentApp.Attribute.FONT_FAMILY]: 'Comic Sans MS'
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
body = doc.getBody()
|
|
28
|
+
body.setAttributes(attributesToSet);
|
|
29
|
+
doc = scl(doc)
|
|
30
|
+
d = Docs.Documents.get(id)
|
|
31
|
+
console.log(JSON.stringify(d))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
deleteTempFile(id)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
tx2()
|
|
38
|
+
const tx1 = () => {
|
|
39
|
+
let doc = DocumentApp.create("abc")
|
|
40
|
+
const id = doc.getId()
|
|
41
|
+
moveToTempFolder(id, suffix)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
const body = doc.getBody();
|
|
46
|
+
const paraText = "p1";
|
|
47
|
+
|
|
48
|
+
const appendedPara = body.appendParagraph(paraText);
|
|
49
|
+
const paraToTest = body.getChild(1);
|
|
50
|
+
doc = scl(doc)
|
|
51
|
+
console.log(report(Docs.Documents.get(id, { includeTabsContent: true }), `\n1.empty document`))
|
|
52
|
+
const attributes = paraToTest.getAttributes();
|
|
53
|
+
|
|
54
|
+
deleteTempFile(id)
|
|
55
|
+
}
|
|
56
|
+
tx1()
|
|
7
57
|
const tabsa = () => {
|
|
8
58
|
|
|
9
59
|
let doc = DocumentApp.create("abc")
|
|
@@ -11,11 +61,11 @@ const tabsa = () => {
|
|
|
11
61
|
moveToTempFolder(id, suffix)
|
|
12
62
|
|
|
13
63
|
let body = doc.getBody()
|
|
14
|
-
console.log
|
|
64
|
+
console.log(body)
|
|
15
65
|
doc = scl(doc)
|
|
16
|
-
console.log(report(Docs.Documents.get(id, {includeTabsContent: true}), `\n1.empty document`))
|
|
66
|
+
console.log(report(Docs.Documents.get(id, { includeTabsContent: true }), `\n1.empty document`))
|
|
67
|
+
|
|
17
68
|
|
|
18
|
-
|
|
19
69
|
deleteTempFile(id)
|
|
20
70
|
}
|
|
21
71
|
|
|
@@ -70,7 +120,7 @@ const pbnew = () => {
|
|
|
70
120
|
console.log(report(Docs.Documents.get(id), `\n4.inserted para 2`))
|
|
71
121
|
|
|
72
122
|
body = doc.getBody()
|
|
73
|
-
body.insertTable(1, [['eboo', 'fbar']]
|
|
123
|
+
body.insertTable(1, [['eboo', 'fbar']])
|
|
74
124
|
doc = scl(doc)
|
|
75
125
|
console.log(report(Docs.Documents.get(id), `\nt4.inserted table at child 1`))
|
|
76
126
|
|
|
@@ -81,12 +131,12 @@ const pbnew = () => {
|
|
|
81
131
|
|
|
82
132
|
body = doc.getBody()
|
|
83
133
|
let c = body.getChild(2)
|
|
84
|
-
c.appendText(
|
|
134
|
+
c.appendText('p2 appended text')
|
|
85
135
|
doc = scl(doc)
|
|
86
136
|
console.log(report(Docs.Documents.get(id), `\n6.appended text to para 2`))
|
|
87
137
|
|
|
88
138
|
body = doc.getBody()
|
|
89
|
-
body.insertTable(3, [['bar', 'foo'], ['barfoo', 'foobar'], ['eboo', 'fbar']]
|
|
139
|
+
body.insertTable(3, [['bar', 'foo'], ['barfoo', 'foobar'], ['eboo', 'fbar']])
|
|
90
140
|
doc = scl(doc)
|
|
91
141
|
console.log(report(Docs.Documents.get(id), `\nt6.inserted table at child 3`))
|
|
92
142
|
|
|
@@ -107,7 +157,7 @@ const pbnew = () => {
|
|
|
107
157
|
console.log(report(Docs.Documents.get(id), `\n9.inserted page break in para 3`))
|
|
108
158
|
|
|
109
159
|
body = doc.getBody()
|
|
110
|
-
c=body.getChild(3)
|
|
160
|
+
c = body.getChild(3)
|
|
111
161
|
c.appendText('after pagebreak in 3')
|
|
112
162
|
doc = scl(doc)
|
|
113
163
|
console.log(report(Docs.Documents.get(id), `\n10.inserted text after pagebreak in 3`))
|
|
@@ -120,7 +170,7 @@ const pbnew = () => {
|
|
|
120
170
|
deleteTempFile(id)
|
|
121
171
|
}
|
|
122
172
|
|
|
123
|
-
|
|
173
|
+
|
|
124
174
|
|
|
125
175
|
|
|
126
176
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Compares two values using Google Sheets' ascending sort logic.
|
|
4
|
+
* @param {*} a first value to compare
|
|
5
|
+
* @param {*} b second value to compare
|
|
6
|
+
* @returns {number} a<b: -1, a===b: 0, a>b: 1
|
|
7
|
+
*/
|
|
8
|
+
const compareMixedValues = (a, b) => {
|
|
9
|
+
const isBlankA = (a === null || a === undefined || a === "");
|
|
10
|
+
const isBlankB = (b === null || b === undefined || b === "");
|
|
11
|
+
|
|
12
|
+
// Handle blanks - always sorted to the top in this ascending comparator
|
|
13
|
+
if (isBlankA && !isBlankB) return -1;
|
|
14
|
+
if (!isBlankA && isBlankB) return 1;
|
|
15
|
+
if (isBlankA && isBlankB) return 0;
|
|
16
|
+
|
|
17
|
+
// Get type priorities (lower number = sorts first)
|
|
18
|
+
const getTypePriority = (val) => {
|
|
19
|
+
// Correct Google Sheets ascending priority
|
|
20
|
+
if (typeof val === 'number') return 1;
|
|
21
|
+
if (typeof val === 'string') return 2;
|
|
22
|
+
if (typeof val === 'boolean') return 3;
|
|
23
|
+
if (val instanceof Date) return 4;
|
|
24
|
+
return 5;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const priorityA = getTypePriority(a);
|
|
28
|
+
const priorityB = getTypePriority(b);
|
|
29
|
+
|
|
30
|
+
// If different types, sort by type priority
|
|
31
|
+
if (priorityA !== priorityB) {
|
|
32
|
+
return priorityA < priorityB ? -1 : 1;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Same types - compare values
|
|
36
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
37
|
+
return a - b;
|
|
38
|
+
}
|
|
39
|
+
if (typeof a === 'string' && typeof b === 'string') {
|
|
40
|
+
return a.localeCompare(b, undefined, { sensitivity: 'base', numeric: true });
|
|
41
|
+
}
|
|
42
|
+
if (typeof a === 'boolean' && typeof b === 'boolean') {
|
|
43
|
+
return (a === b) ? 0 : (a ? 1 : -1);
|
|
44
|
+
}
|
|
45
|
+
if (a instanceof Date && b instanceof Date) {
|
|
46
|
+
return a.getTime() - b.getTime();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fallback for other types
|
|
50
|
+
const strA = String(a);
|
|
51
|
+
const strB = String(b);
|
|
52
|
+
return strA.localeCompare(strB, undefined, { sensitivity: 'base' });
|
|
53
|
+
};
|
|
54
|
+
// ... your sort2d function remains correct, just pass the 'ascending' flag to compareMixedValues.
|
|
55
|
+
export const sort2d = (spec, arr) => {
|
|
56
|
+
const deepCopy = arr.map(row => [...row]);
|
|
57
|
+
return deepCopy.sort((a, b) => {
|
|
58
|
+
for (const s of spec) {
|
|
59
|
+
const index = (typeof s === 'object' && s !== null) ? s.column - 1 : s - 1;
|
|
60
|
+
const ascending = (typeof s === 'object' && s !== null) ? s.ascending !== false : true;
|
|
61
|
+
let result = compareMixedValues(a[index], b[index]);
|
|
62
|
+
if (!ascending) {
|
|
63
|
+
result = -result;
|
|
64
|
+
}
|
|
65
|
+
if (result !== 0) {
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Test function to verify behavior
|
|
74
|
+
export const testSorting = (spec) => {
|
|
75
|
+
const testData = [
|
|
76
|
+
[false, 120, 'cheese', 'peach', 'armpit', 10],
|
|
77
|
+
['kiwi', 'orange', 'apple', 'bub ble', 'bucket', 'armpit'],
|
|
78
|
+
['plum', 'butter', 'buckle', 'grape', 65, 65],
|
|
79
|
+
[false, "", 'armpit', 'orange', 21, 'buckle'],
|
|
80
|
+
['cherry', 'foo', 'butter', 21, 'red eye', 'foo'],
|
|
81
|
+
['kiwi', 77, false, 'banana', 'red eye', 'apple'],
|
|
82
|
+
['buckle', 'bar', 'foo', 21, 10, 'foo'],
|
|
83
|
+
['bub ble', 65, 'butter', 'buckle', 'bub ble', null],
|
|
84
|
+
['cheese', 'pear', 'pear', 77, 10, 'foo'],
|
|
85
|
+
[
|
|
86
|
+
'bub ble',
|
|
87
|
+
'armpit',
|
|
88
|
+
'melon',
|
|
89
|
+
'buckle',
|
|
90
|
+
'red eye',
|
|
91
|
+
3.141592653589793
|
|
92
|
+
],
|
|
93
|
+
['pear', false, 0.3963452962629548, 'foo', 'banana', 'plum'],
|
|
94
|
+
['cherry', true, 'apple', 'cheese', 'butter', 'peach']
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
console.log('Original:', testData);
|
|
98
|
+
|
|
99
|
+
// Sort by column 2 (booleans) ascending
|
|
100
|
+
const sorted = sort2d(spec, testData);
|
|
101
|
+
console.log('Sorted by column 2 (booleans ASC):', sorted);
|
|
102
|
+
|
|
103
|
+
return sorted;
|
|
104
|
+
};
|
|
105
|
+
const spec = [1,{column:2 , ascending:false}]
|
|
106
|
+
testSorting(spec)
|
|
@@ -1,12 +1,7 @@
|
|
|
1
1
|
import "../../main.js";
|
|
2
|
-
import { FakeSpreadsheetApp } from "../../src/services/spreadsheetapp/fakespreadsheetapp.js";
|
|
3
|
-
import { moveToTempFolder, deleteTempFile } from "./tempfolder.js";
|
|
4
2
|
|
|
5
|
-
|
|
3
|
+
ScriptApp.__behavior.sandBoxMode = true;
|
|
4
|
+
const spreadsheet = SpreadsheetApp.create("sample1");
|
|
6
5
|
const sheet = spreadsheet.getSheets()[0];
|
|
7
6
|
sheet.setName("sample");
|
|
8
|
-
|
|
9
|
-
const r = spreadsheet.getRangeByName("test111");
|
|
10
|
-
if (r) {
|
|
11
|
-
console.log(r.getA1Notation());
|
|
12
|
-
}
|
|
7
|
+
ScriptApp.__behavior.trash();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import "../../main.js";
|
|
2
|
+
|
|
3
|
+
ScriptApp.__behavior.sandBoxMode = true;
|
|
4
|
+
sample();
|
|
5
|
+
ScriptApp.__behavior.trash();
|
|
6
|
+
|
|
7
|
+
function sample() {
|
|
8
|
+
const spreadsheet = SpreadsheetApp.create("sample");
|
|
9
|
+
const sheet = spreadsheet.getSheets()[0];
|
|
10
|
+
|
|
11
|
+
// Create protected sheet.
|
|
12
|
+
// For Class Sheet
|
|
13
|
+
const unprotectedRanges = [sheet.getRange("A1:B1")];
|
|
14
|
+
const p1 = sheet
|
|
15
|
+
.protect()
|
|
16
|
+
.setDescription("sample1")
|
|
17
|
+
.setWarningOnly(true)
|
|
18
|
+
.setUnprotectedRanges(unprotectedRanges);
|
|
19
|
+
console.log(p1.getDescription()); // "sample1"
|
|
20
|
+
console.log(p1.getUnprotectedRanges()[0].getA1Notation()); // "A1:B1"
|
|
21
|
+
console.log(p1.canEdit()); // true
|
|
22
|
+
console.log(p1.getEditors().length); // 1
|
|
23
|
+
console.log(p1.getProtectionType().toString()); // SHEET
|
|
24
|
+
console.log(p1.isWarningOnly()); // true
|
|
25
|
+
|
|
26
|
+
// For Class Spreadsheet
|
|
27
|
+
const protections = spreadsheet.getProtections(
|
|
28
|
+
SpreadsheetApp.ProtectionType.SHEET
|
|
29
|
+
);
|
|
30
|
+
console.log(protections.some((p) => p.getDescription() == "sample1")); // true
|
|
31
|
+
|
|
32
|
+
p1.remove();
|
|
33
|
+
|
|
34
|
+
// Create protected range with a range.
|
|
35
|
+
// For Class Range
|
|
36
|
+
const p2 = sheet.getRange("A1:D5").protect().setDescription("sample2");
|
|
37
|
+
p2.setRange(sheet.getRange("B2:E6"));
|
|
38
|
+
console.log(p2.getRange().getA1Notation()); // "B2:E6"
|
|
39
|
+
console.log(p2.getProtectionType().toString()); // RANGE
|
|
40
|
+
p2.remove();
|
|
41
|
+
|
|
42
|
+
// Create named range.
|
|
43
|
+
const rangeName = "sampleNamedRange1";
|
|
44
|
+
const range1 = sheet.getRange("C3:F7");
|
|
45
|
+
spreadsheet.setNamedRange(rangeName, range1);
|
|
46
|
+
const namedRange = spreadsheet
|
|
47
|
+
.getNamedRanges()
|
|
48
|
+
.find((e) => e.getName() == rangeName);
|
|
49
|
+
|
|
50
|
+
// Create protected range with named range 1.
|
|
51
|
+
const p3 = sheet.getRange("A1:D5").protect().setDescription("sample2");
|
|
52
|
+
p3.setRangeName(rangeName);
|
|
53
|
+
console.log(p3.getRangeName()); // sampleNamedRange1
|
|
54
|
+
p3.remove();
|
|
55
|
+
|
|
56
|
+
// Create protected range with named range 2.
|
|
57
|
+
const p4 = sheet.getRange("A1:D5").protect().setDescription("sample2");
|
|
58
|
+
p4.setNamedRange(namedRange);
|
|
59
|
+
console.log(p4.getRangeName()); // sampleNamedRange1
|
|
60
|
+
p4.remove();
|
|
61
|
+
|
|
62
|
+
namedRange.remove();
|
|
63
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# A shell script to create a GitHub issue for the image sizing inconsistency.
|
|
4
|
+
|
|
5
|
+
# Issue title
|
|
6
|
+
TITLE="Investigate and Align Image Insertion Sizing Behavior"
|
|
7
|
+
|
|
8
|
+
# Issue body in markdown
|
|
9
|
+
BODY=$(cat <<'EOF'
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
There is a significant and inconsistent discrepancy in how the live `DocumentApp` service and the `gas-fakes` emulation (via the Docs API) handle the dimensions of an image when it is inserted using `body.insertImage(image.copy())` or `body.appendImage(image.copy())`.
|
|
13
|
+
|
|
14
|
+
The `testdocsimages.js` test suite has repeatedly failed and required adjustments because the expected dimensions of the inserted image are unpredictable in the live environment.
|
|
15
|
+
|
|
16
|
+
## Observed Behaviors in Live Apps Script
|
|
17
|
+
|
|
18
|
+
Our testing has revealed several conflicting behaviors from the live API:
|
|
19
|
+
|
|
20
|
+
1. **Intrinsic Size:** Sometimes, the API ignores the dimensions of the copied `InlineImage` object and re-fetches the image from its source URI, using its original intrinsic dimensions (e.g., `544x184` for the Google logo).
|
|
21
|
+
2. **Copied Object Size:** At other times, the API correctly respects the dimensions of the copied `InlineImage` object (e.g., `61x181` in our tests).
|
|
22
|
+
3. **Default/Fixed Size:** On at least one occasion, the API appeared to resize the image to a seemingly arbitrary fixed size (e.g., `240x80`).
|
|
23
|
+
4. **State-Dependent:** The behavior seems to be dependent on the state of the document. A brand-new document might exhibit one behavior, while a reused document (even after `doc.clear()`) exhibits another.
|
|
24
|
+
|
|
25
|
+
## Current Workaround
|
|
26
|
+
|
|
27
|
+
The current test (`insertImage behavior on new documents` in `testdocsimages.js`) has been made more robust by:
|
|
28
|
+
- Forcing the creation of a new document for the test using `maketdoc(..., { forceNew: true })`.
|
|
29
|
+
- Abandoning exact dimension checks in the live environment and instead verifying the image's **aspect ratio** within a tolerance.
|
|
30
|
+
|
|
31
|
+
While this makes the test pass, it highlights a core emulation inaccuracy.
|
|
32
|
+
|
|
33
|
+
## TODO
|
|
34
|
+
|
|
35
|
+
1. **Investigate Further:** Conduct a definitive set of tests to determine if there is *any* predictable pattern to the live API's behavior.
|
|
36
|
+
2. **Decide on Emulation Strategy:**
|
|
37
|
+
- Should `gas-fakes` emulate the most common or most "correct" behavior (i.e., respecting the copied object's dimensions)?
|
|
38
|
+
- Or should it attempt to mimic the most recently observed "new document" behavior (using intrinsic size)?
|
|
39
|
+
3. **Document the Oddity:** Regardless of the chosen strategy, this inconsistency is a significant "oddity" and should be thoroughly documented in `oddities.md` to inform users who might encounter similar issues when working with images in `DocumentApp`.
|
|
40
|
+
|
|
41
|
+
This will improve the accuracy of the fake environment and provide valuable documentation for the community.
|
|
42
|
+
EOF
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Create the issue using GitHub CLI
|
|
46
|
+
gh issue create --title "$TITLE" --body "$BODY" --label "bug" --label "emulation-accuracy" --label "document-app"
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# A shell script to create a GitHub issue for the FormApp.create() title inconsistency.
|
|
4
|
+
|
|
5
|
+
# Issue title
|
|
6
|
+
TITLE="Discrepancy between FormApp.create(title) and Forms API behavior regarding form title and file name"
|
|
7
|
+
|
|
8
|
+
# Issue body in markdown
|
|
9
|
+
BODY=$(cat <<'EOF'
|
|
10
|
+
### Summary
|
|
11
|
+
|
|
12
|
+
There is a significant and counter-intuitive discrepancy between the documented behavior of `FormApp.create(title)` in Google Apps Script and the constraints of the underlying Google Forms API. `FormApp.create(title)` sets the Google Drive *file name* to the provided `title`, but leaves the form's internal title (retrieved by `form.getTitle()`) blank.
|
|
13
|
+
|
|
14
|
+
This makes it impossible to accurately emulate the Apps Script behavior using the public Forms API, as the API requires a non-empty title for both creation and updates.
|
|
15
|
+
|
|
16
|
+
### Live Apps Script Behavior
|
|
17
|
+
|
|
18
|
+
When the following Apps Script code is executed:
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
function testFormCreation() {
|
|
22
|
+
const formName = "foo-form";
|
|
23
|
+
const form = FormApp.create(formName);
|
|
24
|
+
const id = form.getId();
|
|
25
|
+
|
|
26
|
+
console.log('Internal form title:', form.getTitle()); // Logs: "" (an empty string)
|
|
27
|
+
|
|
28
|
+
const file = DriveApp.getFileById(id);
|
|
29
|
+
console.log('Drive file name:', file.getName()); // Logs: "foo-form"
|
|
30
|
+
|
|
31
|
+
// cleanup
|
|
32
|
+
file.setTrashed(true);
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The output demonstrates that `FormApp.create()` uses the argument to set the Drive file name, but the form's own title property remains empty.
|
|
37
|
+
|
|
38
|
+
### The Problem with API Emulation
|
|
39
|
+
|
|
40
|
+
This behavior is problematic to replicate with the v1 Google Forms API because the `forms.create` method requires a non-empty `info.title`. Attempting to then `batchUpdate` the title to an empty string also fails, as the API rejects an empty title on update.
|
|
41
|
+
|
|
42
|
+
### Conclusion
|
|
43
|
+
|
|
44
|
+
This disconnect means it's impossible for developers using the public Forms API to programmatically create a form that exactly matches the state of one created by `FormApp.create()`. This poses a significant challenge for testing and emulation frameworks.
|
|
45
|
+
|
|
46
|
+
The expected behavior would be for `FormApp.create(title)` to set both the Drive file name and the internal form title, or for the API to allow setting the title to an empty string via an update.
|
|
47
|
+
EOF
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Create the issue using GitHub CLI
|
|
51
|
+
gh issue create --title "$TITLE" --body "$BODY" --label "bug" --label "emulation-accuracy" --label "form-app"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# A shell script to create a GitHub issue for the Protection.getEditors() behavior.
|
|
4
|
+
|
|
5
|
+
# Issue title
|
|
6
|
+
TITLE="Align Protection.getEditors() behavior with Live Apps Script"
|
|
7
|
+
|
|
8
|
+
# Issue body in markdown
|
|
9
|
+
BODY=$(cat <<'EOF'
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
There is a discrepancy between the live Google Apps Script environment and the `gas-fakes` emulation regarding the return value of `Protection.getEditors()` on a newly created protection.
|
|
13
|
+
|
|
14
|
+
- **Live Apps Script:** `protection.getEditors().length` returns `0`.
|
|
15
|
+
- **`gas-fakes` (current):** `protection.getEditors().length` incorrectly returns `1`.
|
|
16
|
+
|
|
17
|
+
## Live Behavior Details
|
|
18
|
+
|
|
19
|
+
In the live environment, the owner of a spreadsheet can always edit a protected range or sheet (`protection.canEdit()` returns `true`). However, the owner is **not** automatically included in the explicit list of editors returned by `getEditors()`. This is confirmed by live testing.
|
|
20
|
+
|
|
21
|
+
## `gas-fakes` Behavior
|
|
22
|
+
|
|
23
|
+
The `gas-fakes` implementation in `fakeprotection.js` was incorrectly adding the current user (as the owner) to the list of editors upon creation of a new `Protection` object.
|
|
24
|
+
|
|
25
|
+
## TODO
|
|
26
|
+
|
|
27
|
+
1. Modify `fakeprotection.js` to no longer automatically add the owner to the `editors` list when a new protection is created. The `canEdit()` method should still return `true` for the owner.
|
|
28
|
+
2. Run the full test suite to ensure this change doesn't have unintended side effects and that the `Protection` tests now pass in both environments.
|
|
29
|
+
EOF
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Create the issue using GitHub CLI
|
|
33
|
+
gh issue create --title "$TITLE" --body "$BODY" --label "bug" --label "emulation-accuracy" --label "spreadsheet-app"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# A shell script to create a GitHub issue for reviewing sandbox listing behavior.
|
|
4
|
+
|
|
5
|
+
# Issue title
|
|
6
|
+
TITLE="Review Sandbox Strategy for File Listing Operations"
|
|
7
|
+
|
|
8
|
+
# Issue body in markdown
|
|
9
|
+
BODY=$(cat <<'EOF'
|
|
10
|
+
## Summary
|
|
11
|
+
|
|
12
|
+
Currently, tests that perform broad file/folder listing operations (e.g., `DriveApp.getFiles()`, `someFolder.getFolders()`) fail when `strictSandbox` is `true`. This is because the underlying iterators may need to access metadata of parent folders that are not explicitly whitelisted, causing the sandbox to correctly deny access.
|
|
13
|
+
|
|
14
|
+
The current workaround, as implemented in `testdrive.js`, is to temporarily disable `strictSandbox` for the duration of these tests:
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
const wasStrict = behavior.strictSandbox;
|
|
18
|
+
behavior.strictSandbox = false;
|
|
19
|
+
try {
|
|
20
|
+
// ... listing operations ...
|
|
21
|
+
} finally {
|
|
22
|
+
behavior.strictSandbox = wasStrict;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
While this works, it feels like a patch rather than a fundamental solution. We should review if there's a more robust and elegant way to handle this.
|
|
27
|
+
|
|
28
|
+
## TODO
|
|
29
|
+
|
|
30
|
+
Investigate and decide on the best long-term strategy for handling file listings in strict sandbox mode.
|
|
31
|
+
|
|
32
|
+
### Options to Consider:
|
|
33
|
+
|
|
34
|
+
1. **Implicit Parent Whitelisting:** Could the file/folder iterators be enhanced to automatically (and temporarily) whitelist parent folders as they are encountered? This would keep the sandbox strict for all other operations but allow listings to complete. This seems like the most robust solution if feasible.
|
|
35
|
+
|
|
36
|
+
2. **Refined Iterator Logic:** Can the iterators be modified to avoid the specific checks that cause the failure when in strict mode? This might involve fetching less metadata for items outside the session/whitelist scope.
|
|
37
|
+
|
|
38
|
+
3. **Accept and Document:** If the current `try...finally` workaround is deemed the most practical approach, we should formally document it in `sandbox.md` as the recommended pattern for this scenario.
|
|
39
|
+
|
|
40
|
+
This review will help ensure the sandbox feature is both as secure and as user-friendly as possible.
|
|
41
|
+
EOF
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# Create the issue using GitHub CLI
|
|
45
|
+
gh issue create --title "$TITLE" --body "$BODY" --label "enhancement"
|
package/package.json
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
},
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@mcpher/fake-gasenum": "^1.0.2",
|
|
7
|
-
"@mcpher/gas-fakes": "^1.0.14",
|
|
8
7
|
"@mcpher/unit": "^1.1.11",
|
|
9
8
|
"@sindresorhus/is": "^7.0.1",
|
|
10
9
|
"archiver": "^7.0.1",
|
|
@@ -43,6 +42,8 @@
|
|
|
43
42
|
"testsheetsdata": "cp mainlocal.js main.js && node --env-file=.env ./test/testsheetsdata.js execute",
|
|
44
43
|
"testdocsadv": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsadv.js execute",
|
|
45
44
|
"testslidesadv": "cp mainlocal.js main.js && node --env-file=.env ./test/testslidesadv.js execute",
|
|
45
|
+
"testform": "cp mainlocal.js main.js && node --env-file=.env ./test/testform.js execute",
|
|
46
|
+
"testformsadv": "cp mainlocal.js main.js && node --env-file=.env ./test/testformsadv.js execute",
|
|
46
47
|
"testdocs": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocs.js execute",
|
|
47
48
|
"testslides": "cp mainlocal.js main.js && node --env-file=.env ./test/testslides.js execute",
|
|
48
49
|
"testdocsnext": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsnext.js execute",
|
|
@@ -54,10 +55,12 @@
|
|
|
54
55
|
"testdocsfooters": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsfooters.js execute",
|
|
55
56
|
"testdocsfootnotes": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsfootnotes.js execute",
|
|
56
57
|
"testdocsimages": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsimages.js execute",
|
|
58
|
+
"testdocsstyles": "cp mainlocal.js main.js && node --env-file=.env ./test/testdocsstyles.js execute",
|
|
59
|
+
"testsandbox": "cp mainlocal.js main.js && node --env-file=.env ./test/testsandbox.js execute",
|
|
57
60
|
"pub": "cp mainlocal.js main.js && npm publish --access public"
|
|
58
61
|
},
|
|
59
62
|
"name": "@mcpher/gas-fakes",
|
|
60
|
-
"version": "1.0.
|
|
63
|
+
"version": "1.0.19",
|
|
61
64
|
"license": "MIT",
|
|
62
65
|
"main": "main.js",
|
|
63
66
|
"description": "A proof of concept implementation of Apps Script Environment on Node",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# This script contains the prompts to regenerate the progress reports for all services.
|
|
5
|
+
# Execute this script and paste the output to the Gemini agent.
|
|
6
|
+
|
|
7
|
+
echo "Generating progress report for Docs service..."
|
|
8
|
+
echo "now do https://developers.google.com/apps-script/reference/document into ./progress/Docs.MD"
|
|
9
|
+
|
|
10
|
+
echo "Generating progress report for Drive service..."
|
|
11
|
+
echo "now do https://developers.google.com/apps-script/reference/drive into ./progress/drive.MD"
|
|
12
|
+
|
|
13
|
+
echo "Generating progress report for Forms service..."
|
|
14
|
+
echo "now do https://developers.google.com/apps-script/reference/forms into ./progress/forms.MD"
|
|
15
|
+
|
|
16
|
+
echo "Generating progress report for Slides service..."
|
|
17
|
+
echo "now do https://developers.google.com/apps-script/reference/slides into ./progress/slides.MD"
|
|
18
|
+
|
|
19
|
+
echo "Generating progress report for Spreadsheet service..."
|
|
20
|
+
echo "now do https://developers.google.com/apps-script/reference/spreadsheet into ./progress/sheets.MD"
|
|
21
|
+
|
|
22
|
+
echo "Generating progress report for Utilities service..."
|
|
23
|
+
echo "now do https://developers.google.com/apps-script/reference/utilities into ./progress/utilities.MD"
|
|
24
|
+
|
|
25
|
+
echo "Generating progress report for UrlFetch service..."
|
|
26
|
+
echo "now do https://developers.google.com/apps-script/reference/url-fetch into ./progress/urlFetch.MD"
|
|
27
|
+
|
|
28
|
+
echo "Generating progress report for Cache service..."
|
|
29
|
+
echo "now do https://developers.google.com/apps-script/reference/cache into ./progress/cache.MD"
|
|
30
|
+
|
|
31
|
+
echo "Generating progress report for Properties service..."
|
|
32
|
+
echo "now do https://developers.google.com/apps-script/reference/properties into ./progress/properties.MD"
|
|
33
|
+
|
|
34
|
+
echo "Generating progress report for Script service..."
|
|
35
|
+
echo "now do https://developers.google.com/apps-script/reference/script into ./progress/script.MD"
|
|
36
|
+
|
|
37
|
+
echo "Generating progress report for Base service..."
|
|
38
|
+
echo "now do https://developers.google.com/apps-script/reference/base into ./progress/base.MD"
|
package/src/index.js
CHANGED
|
@@ -4,10 +4,12 @@ import './services/urlfetchapp/app.js'
|
|
|
4
4
|
import './services/utilities/app.js'
|
|
5
5
|
import './services/spreadsheetapp/app.js'
|
|
6
6
|
import './services/stores/app.js'
|
|
7
|
-
import './services/
|
|
7
|
+
import './services/base/app.js'
|
|
8
8
|
import './services/advdrive/app.js'
|
|
9
9
|
import './services/advsheets/app.js'
|
|
10
10
|
import './services/advdocs/app.js'
|
|
11
11
|
import './services/advslides/app.js'
|
|
12
12
|
import './services/documentapp/app.js'
|
|
13
|
+
import './services/advforms/app.js'
|
|
14
|
+
import './services/formapp/app.js'
|
|
13
15
|
import './services/slidesapp/app.js'
|
|
@@ -628,7 +628,12 @@ class FakeAdvDocs {
|
|
|
628
628
|
__getDocsPerformance() {
|
|
629
629
|
return docsCacher.getPerformance()
|
|
630
630
|
}
|
|
631
|
+
__addAllowed(id) {
|
|
632
|
+
if (ScriptApp.__behavior.sandBoxMode) {
|
|
633
|
+
ScriptApp.__behavior.addFile(id);
|
|
634
|
+
}
|
|
635
|
+
return id
|
|
636
|
+
}
|
|
631
637
|
}
|
|
632
638
|
|
|
633
639
|
export const newFakeAdvDocs = (...args) => Proxies.guard(new FakeAdvDocs(...args))
|
|
634
|
-
|
|
@@ -17,19 +17,7 @@ class FakeAdvDocuments extends FakeAdvResource {
|
|
|
17
17
|
constructor(docs) {
|
|
18
18
|
super(docs, 'documents', Syncit.fxDocs);
|
|
19
19
|
this.__fakeObjectType = "Docs.Documents";
|
|
20
|
-
|
|
21
|
-
// in sandbox mode only files created in this instance are
|
|
22
|
-
__allowed(id) {
|
|
23
|
-
if (!ScriptApp.__behavior.isAccessible(id)) {
|
|
24
|
-
throw new Error(`Access to document ${id} is not allowed in sandbox mode`);
|
|
25
|
-
}
|
|
26
|
-
return id
|
|
27
|
-
}
|
|
28
|
-
__addAllowed(id) {
|
|
29
|
-
if (ScriptApp.__behavior.sandBoxMode) {
|
|
30
|
-
ScriptApp.__behavior.addFile(id);
|
|
31
|
-
}
|
|
32
|
-
return id
|
|
20
|
+
this.docs = docs;
|
|
33
21
|
}
|
|
34
22
|
|
|
35
23
|
create(resource, options) {
|
|
@@ -44,7 +32,7 @@ class FakeAdvDocuments extends FakeAdvResource {
|
|
|
44
32
|
|
|
45
33
|
// maybe we need to throw an error
|
|
46
34
|
gError(response, 'docs.documents', "create")
|
|
47
|
-
this.__addAllowed(data.documentId);
|
|
35
|
+
this.docs.__addAllowed(data.documentId);
|
|
48
36
|
return data
|
|
49
37
|
}
|
|
50
38
|
|
|
@@ -55,12 +43,14 @@ class FakeAdvDocuments extends FakeAdvResource {
|
|
|
55
43
|
matchThrow("API call to docs.documents.batchUpdate failed with error: Invalid JSON payload received.");
|
|
56
44
|
}
|
|
57
45
|
|
|
46
|
+
// any update to a doc is a write
|
|
47
|
+
ScriptApp.__behavior.isAccessible(documentId, 'Docs', 'write');
|
|
58
48
|
|
|
59
49
|
// Invalidate the cache for this document since we are updating it.
|
|
60
50
|
docsCacher.clear(documentId);
|
|
61
51
|
// console.log (JSON.stringify(requests))
|
|
62
52
|
const { response, data } = this._call("batchUpdate", {
|
|
63
|
-
documentId
|
|
53
|
+
documentId,
|
|
64
54
|
requestBody: requests
|
|
65
55
|
});
|
|
66
56
|
|
|
@@ -101,7 +91,8 @@ class FakeAdvDocuments extends FakeAdvResource {
|
|
|
101
91
|
|
|
102
92
|
if (is.nonEmptyObject(options) && !Reflect.ownKeys(options || {}).every(f => optionsSet.has(f))) matchThrow();
|
|
103
93
|
|
|
104
|
-
|
|
94
|
+
ScriptApp.__behavior.isAccessible(documentId, 'Docs', 'read');
|
|
95
|
+
const params = {documentId, ...(options || {}) };
|
|
105
96
|
|
|
106
97
|
const { response, data } = this._call("get", params);
|
|
107
98
|
gError(response, 'docs.documents', 'get');
|