@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.
Files changed (71) hide show
  1. package/.aiexclude +6 -0
  2. package/gasmess/bruce/pbx.js +58 -8
  3. package/gasmess/bruce/sorting.js +106 -0
  4. package/gasmess/tanaike/for_simple_test.js +3 -8
  5. package/gasmess/tanaike/sample5.js +63 -0
  6. package/ghissues/image-size-inconsistency-issue.sh +46 -0
  7. package/ghissues/issue-formapp-create-title-inconsistency.sh +51 -0
  8. package/ghissues/protection-editors-issue.sh +33 -0
  9. package/ghissues/review-sandbox-listing-issue.sh +45 -0
  10. package/package.json +5 -2
  11. package/regenerate-progress-reports.sh +38 -0
  12. package/src/index.js +3 -1
  13. package/src/services/advdocs/fakeadvdocs.js +6 -1
  14. package/src/services/advdocs/fakeadvdocuments.js +7 -16
  15. package/src/services/advdrive/fakeadvdrivefiles.js +13 -3
  16. package/src/services/advdrive/fakeadvdrivepermissions.js +6 -3
  17. package/src/services/advforms/app.js +31 -0
  18. package/src/services/advforms/fakeadvforms.js +221 -0
  19. package/src/services/advforms/fakeadvformsform.js +80 -0
  20. package/src/services/advforms/formsapis.js +15 -0
  21. package/src/services/advsheets/fakeadvsheets.js +0 -7
  22. package/src/services/advsheets/fakeadvsheetsdevelopermetadata.js +4 -2
  23. package/src/services/advsheets/fakeadvsheetsspreadsheets.js +18 -5
  24. package/src/services/advsheets/fakeadvsheetsvalues.js +4 -2
  25. package/src/services/advslides/fakeadvslides.js +10 -12
  26. package/src/services/documentapp/app.js +25 -3
  27. package/src/services/documentapp/appenderhelpers.js +32 -2
  28. package/src/services/documentapp/elementblasters.js +78 -2
  29. package/src/services/documentapp/elementhelpers.js +106 -1
  30. package/src/services/documentapp/elementoptions.js +98 -17
  31. package/src/services/documentapp/fakebody.js +244 -1
  32. package/src/services/documentapp/fakedocument.js +55 -4
  33. package/src/services/documentapp/fakedocumentapp.js +23 -6
  34. package/src/services/documentapp/fakelistitem.js +72 -1
  35. package/src/services/documentapp/fakeparagraph.js +101 -63
  36. package/src/services/documentapp/shadowdocument.js +27 -33
  37. package/src/services/documentapp/stylehelpers.js +1 -0
  38. package/src/services/driveapp/fakedriveapp.js +0 -6
  39. package/src/services/driveapp/fakedrivefile.js +2 -3
  40. package/src/services/driveapp/fakefolderapp.js +1 -1
  41. package/src/services/enums/docsenums.js +2 -1
  42. package/src/services/enums/formsenums.js +62 -0
  43. package/src/services/formapp/app.js +44 -0
  44. package/src/services/formapp/fakecheckboxitem.js +142 -0
  45. package/src/services/formapp/fakechoice.js +45 -0
  46. package/src/services/formapp/fakeform.js +167 -0
  47. package/src/services/formapp/fakeformapp.js +107 -0
  48. package/src/services/formapp/fakeformitem.js +218 -0
  49. package/src/services/formapp/formitemregistry.js +23 -0
  50. package/src/services/formapp/formitems.js +6 -0
  51. package/src/services/scriptapp/app.js +2 -1
  52. package/src/services/scriptapp/behavior.js +291 -11
  53. package/src/services/slidesapp/app.js +25 -4
  54. package/src/services/slidesapp/fakepresentation.js +3 -1
  55. package/src/services/slidesapp/fakeslidesapp.js +5 -1
  56. package/src/services/spreadsheetapp/app.js +26 -9
  57. package/src/services/spreadsheetapp/fakeprotection.js +448 -0
  58. package/src/services/spreadsheetapp/fakesheet.js +63 -9
  59. package/src/services/spreadsheetapp/fakesheetrange.js +25 -1
  60. package/src/services/spreadsheetapp/fakespreadsheet.js +41 -2
  61. package/src/services/spreadsheetapp/fakespreadsheetapp.js +9 -1
  62. package/src/support/formscacher.js +7 -0
  63. package/src/support/helpers.js +1 -3
  64. package/src/support/proxies.js +14 -3
  65. package/src/support/sxdocs.js +3 -3
  66. package/src/support/sxforms.js +50 -0
  67. package/src/support/syncit.js +4 -0
  68. package/src/support/workersync/sxfunctions.js +1 -0
  69. /package/{fakepositionedimage.js → fakeadvdocuments.js} +0 -0
  70. /package/src/services/{session → base}/app.js +0 -0
  71. /package/src/services/{session → base}/fakesession.js +0 -0
package/.aiexclude ADDED
@@ -0,0 +1,6 @@
1
+ testongas
2
+ gaslibtests
3
+ gasmess
4
+ node_modules
5
+ shells
6
+ README.RU.md
@@ -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 (body)
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( 'p2 appended text')
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
- pbnew()
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
- const spreadsheet = SpreadsheetApp.openById("###");
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.17",
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/session/app.js'
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: this.__allowed(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
- const params = {documentId: this.__allowed(documentId), ...(options || {}) };
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');