@mcpher/gas-fakes 1.2.20 → 1.2.22

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/.clasp.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "scriptId": "",
3
+ "rootDir": "./src/"
4
+ }
package/.claspignore ADDED
@@ -0,0 +1,6 @@
1
+ # Ignore local development files
2
+ run.js
3
+ node_modules/
4
+ package.json
5
+ package-lock.json
6
+ .gemini/
package/ex-logo.png ADDED
Binary file
package/logo.png CHANGED
Binary file
package/package.json CHANGED
@@ -20,6 +20,7 @@
20
20
  "mime": "^4.0.7",
21
21
  "prompts": "^2.4.2",
22
22
  "sleep-synchronously": "^2.0.0",
23
+ "tape": "^5.9.0",
23
24
  "unzipper": "^0.12.3",
24
25
  "zod": "^3.25.76"
25
26
  },
@@ -32,7 +33,7 @@
32
33
  },
33
34
  "name": "@mcpher/gas-fakes",
34
35
  "author": "bruce mcpherson",
35
- "version": "1.2.20",
36
+ "version": "1.2.22",
36
37
  "license": "MIT",
37
38
  "main": "main.js",
38
39
  "description": "A proof of concept implementation of Apps Script Environment on Node",
package/run.js ADDED
@@ -0,0 +1,35 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import vm from 'vm';
4
+
5
+ // Import gas-fakes to initialize the environment
6
+ import '@mcpher/gas-fakes';
7
+
8
+ // Create a sandboxed context
9
+ const context = vm.createContext({
10
+ ScriptApp: global.ScriptApp,
11
+ console: console,
12
+ // Add other GAS globals you need here (e.g., DocumentApp, SpreadsheetApp)
13
+ });
14
+
15
+ // Read the user's script file
16
+ const scriptPath = path.resolve('src/Code.js');
17
+ const scriptCode = fs.readFileSync(scriptPath, 'utf8');
18
+
19
+ // Run the user's script in the sandbox to define the functions
20
+ vm.runInContext(scriptCode, context);
21
+
22
+ // Define and run the calling logic to execute a function from the script
23
+ const callingCode = `
24
+ // 1. Enable sandbox mode for safety
25
+ ScriptApp.__behavior.sandBoxMode = true;
26
+
27
+ // 2. Call the specific function from your Code.js file
28
+ // myFunction();
29
+
30
+ // 3. Clean up the gas-fakes environment
31
+ ScriptApp.__behavior.trash();
32
+ `;
33
+
34
+ // vm.runInContext(callingCode, context);
35
+ console.log("Project initialized. Edit run.js to execute a function.");
package/src/Code.js ADDED
@@ -0,0 +1,3 @@
1
+ function myFunction() {
2
+ console.log('Hello from Google Apps Script!');
3
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "dependencies": {},
3
+ "exceptionLogging": "STACKDRIVER",
4
+ "runtimeVersion": "V8"
5
+ }
@@ -42,7 +42,8 @@ class FakeAdvForms {
42
42
  "newFormInfo": [
43
43
  "title",
44
44
  "documentTitle",
45
- "description"
45
+ "description",
46
+ "state"
46
47
  ],
47
48
  "newUpdateSettingsRequest": [
48
49
  "settings",
@@ -4,6 +4,7 @@ import { signatureArgs, ssError, gError } from '../../support/helpers.js';
4
4
 
5
5
  import { Proxies } from '../../support/proxies.js';
6
6
  import { Utils } from '../../support/utils.js';
7
+ import { newFakeResponses } from './fakeresponses.js';
7
8
  const { is } = Utils
8
9
 
9
10
  export const newFakeAdvFormsForm = (...args) => Proxies.guard(new FakeAdvFormsForm(...args))
@@ -13,6 +14,17 @@ class FakeAdvFormsForm extends FakeAdvResource {
13
14
  super(mainService, 'forms', Syncit.fxForms);
14
15
  this.forms = mainService;
15
16
  this.__fakeObjectType = 'Forms.Forms';
17
+ this.__responses = null;
18
+ }
19
+
20
+ /**
21
+ * @returns {import('./fakeresponses.js').FakeResponses}
22
+ */
23
+ get Responses() {
24
+ if (!this.__responses) {
25
+ this.__responses = newFakeResponses(this);
26
+ }
27
+ return this.__responses;
16
28
  }
17
29
 
18
30
  /**
@@ -0,0 +1,30 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { Utils } from '../../support/utils.js';
3
+
4
+ export const newFakeResponses = (...args) => {
5
+ return Proxies.guard(new FakeResponses(...args));
6
+ };
7
+
8
+ /**
9
+ * @class FakeResponses
10
+ * @see https://developers.google.com/forms/api/reference/rest/v1/forms.responses
11
+ */
12
+ export class FakeResponses {
13
+ constructor(mainService) {
14
+ this.forms = mainService;
15
+ this.__fakeObjectType = 'Forms.Responses';
16
+ }
17
+
18
+ /**
19
+ * Lists the responses of a form.
20
+ * @param {string} formId The form's ID.
21
+ * @returns {{responses: object[]}} A list of responses.
22
+ */
23
+ list(formId) {
24
+ // this.forms is the FakeAdvFormsForm instance, which has the _call method
25
+ // from FakeAdvResource to interact with the syncit data source.
26
+ // We use subProp to specify the nested 'responses' object, following the library's pattern.
27
+ const { data } = this.forms._call('list', { formId }, null, 'responses');
28
+ return data || { responses: [] };
29
+ }
30
+ }
@@ -0,0 +1,105 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { FakeFormItem } from './fakeformitem.js';
3
+ import { registerFormItem } from './formitemregistry.js';
4
+ import { signatureArgs } from '../../support/helpers.js';
5
+ import { Utils } from '../../support/utils.js';
6
+ import { ItemType } from '../enums/formsenums.js';
7
+
8
+ export const newFakeCheckboxGridItem = (...args) => {
9
+ return Proxies.guard(new FakeCheckboxGridItem(...args));
10
+ };
11
+
12
+ /**
13
+ * @class FakeCheckboxGridItem
14
+ * A fake for the CheckboxGridItem class in Apps Script.
15
+ * @see https://developers.google.com/apps-script/reference/forms/checkbox-grid-item
16
+ */
17
+ export class FakeCheckboxGridItem extends FakeFormItem {
18
+ constructor(form, itemId) {
19
+ super(form, itemId);
20
+ }
21
+
22
+ /**
23
+ * Gets the values for every column in the grid.
24
+ * @returns {string[]} an array of column values
25
+ */
26
+ getColumns() {
27
+ const choiceQuestion = this.__resource.questionGroupItem?.grid?.columns;
28
+ if (!choiceQuestion || !choiceQuestion.options) {
29
+ return [];
30
+ }
31
+ return choiceQuestion.options.map(option => option.value);
32
+ }
33
+
34
+ /**
35
+ * Gets the values for every row in the grid.
36
+ * @returns {string[]} an array of row values
37
+ */
38
+ getRows() {
39
+ const questions = this.__resource.questionGroupItem?.questions;
40
+ if (!questions) {
41
+ return [];
42
+ }
43
+ return questions.map(question => question.rowQuestion?.title || null).filter(f => f);
44
+ }
45
+
46
+ /**
47
+ * Sets the columns of the grid based on an array of values.
48
+ * @param {string[]} columns an array of column values
49
+ * @returns {FakeCheckboxGridItem} this item, for chaining
50
+ */
51
+ setColumns(columns) {
52
+ const { nargs, matchThrow } = signatureArgs(arguments, 'CheckboxGridItem.setColumns');
53
+ if (nargs !== 1 || !Utils.is.array(columns) || !columns.every(Utils.is.string)) {
54
+ matchThrow('Invalid arguments: expected a string array.');
55
+ }
56
+ if (columns.length === 0) {
57
+ throw new Error('The array of columns cannot be empty.');
58
+ }
59
+
60
+ const updatedResource = JSON.parse(JSON.stringify(this.__resource));
61
+ updatedResource.questionGroupItem.grid.columns.options = columns.map(c => ({ value: c }));
62
+
63
+ const updateRequest = Forms.newRequest().setUpdateItem({
64
+ item: updatedResource,
65
+ location: { index: this.getIndex() },
66
+ updateMask: 'questionGroupItem.grid.columns.options',
67
+ });
68
+
69
+ return this.__update(updateRequest);
70
+ }
71
+
72
+ /**
73
+ * Sets the rows of the grid based on an array of values.
74
+ * @param {string[]} rows an array of row values
75
+ * @returns {FakeCheckboxGridItem} this item, for chaining
76
+ */
77
+ setRows(rows) {
78
+ const { nargs, matchThrow } = signatureArgs(arguments, 'CheckboxGridItem.setRows');
79
+ if (nargs !== 1 || !Utils.is.array(rows) || !rows.every(Utils.is.string)) {
80
+ matchThrow('Invalid arguments: expected a string array.');
81
+ }
82
+ if (rows.length === 0) {
83
+ throw new Error('The array of rows cannot be empty.');
84
+ }
85
+
86
+ const updatedResource = JSON.parse(JSON.stringify(this.__resource));
87
+ updatedResource.questionGroupItem.questions = rows.map(r => ({
88
+ rowQuestion: { title: r },
89
+ }));
90
+
91
+ const updateRequest = Forms.newRequest().setUpdateItem({
92
+ item: updatedResource,
93
+ location: { index: this.getIndex() },
94
+ updateMask: 'questionGroupItem.questions',
95
+ });
96
+
97
+ return this.__update(updateRequest);
98
+ }
99
+
100
+ toString() {
101
+ return 'CheckboxGridItem';
102
+ }
103
+ }
104
+
105
+ registerFormItem(ItemType.CHECKBOX_GRID, newFakeCheckboxGridItem);
@@ -1,5 +1,5 @@
1
1
  import { Proxies } from '../../support/proxies.js';
2
- import { FakeFormItem } from './fakeformitem.js';
2
+ import { FakeChoiceItem } from './fakechoiceitem.js';
3
3
  import { signatureArgs } from '../../support/helpers.js';
4
4
  import { Utils } from '../../support/utils.js';
5
5
  const { is } = Utils;
@@ -16,7 +16,7 @@ export const newFakeCheckboxItem = (...args) => {
16
16
  * A fake for the CheckboxItem class in Apps Script.
17
17
  * @see https://developers.google.com/apps-script/reference/forms/checkbox-item
18
18
  */
19
- export class FakeCheckboxItem extends FakeFormItem {
19
+ export class FakeCheckboxItem extends FakeChoiceItem {
20
20
  /**
21
21
  * @param {import('./fakeform.js').FakeForm} form The parent form.
22
22
  * @param {string} itemId The ID of the item.
@@ -25,114 +25,6 @@ export class FakeCheckboxItem extends FakeFormItem {
25
25
  super(form, itemId);
26
26
  }
27
27
 
28
- /**
29
- * Creates a new choice for this item.
30
- * @param {string} value The value of the choice.
31
- * @param {boolean} [isCorrect] Whether the choice is correct.
32
- * @returns {import('./fakechoice.js').FakeChoice} The new choice.
33
- */
34
- createChoice(value, isCorrect) {
35
- const { nargs, matchThrow } = signatureArgs(arguments, 'CheckboxItem.createChoice');
36
- if (nargs < 1 || nargs > 2 || !is.string(value) || (nargs === 2 && !is.boolean(isCorrect))) {
37
- matchThrow('Invalid arguments');
38
- }
39
- // For now, navigation is not supported.
40
- return newFakeChoice(value, isCorrect || false, null, null);
41
- }
42
-
43
- /**
44
- * Gets the choices for this item.
45
- * @returns {import('./fakechoice.js').FakeChoice[]} An array of Choice objects.
46
- */
47
- getChoices() {
48
- const choiceQuestion = this.__resource.questionItem?.question?.choiceQuestion;
49
- if (!choiceQuestion || !choiceQuestion.options) {
50
- return [];
51
- }
52
- return choiceQuestion.options.map(option => {
53
- return newFakeChoice(option.value, false, null, null);
54
- });
55
- }
56
-
57
- /**
58
- * Sets the choices for this item.
59
- * @param {import('./fakechoice.js').FakeChoice[]} choices An array of Choice objects.
60
- * @returns {FakeCheckboxItem} This item, for chaining.
61
- */
62
- setChoices(choices) {
63
- const { nargs, matchThrow } = signatureArgs(arguments, 'CheckboxItem.setChoices');
64
- if (nargs !== 1 || !is.array(choices) || !choices.every(c => c.toString() === 'Choice')) {
65
- matchThrow('Invalid arguments: expected an array of Choice objects.');
66
- }
67
-
68
- const newOptions = choices.map(choice => Forms.newOption().setValue(choice.getValue()));
69
- const currentIndex = this._getCurrentIndex();
70
-
71
- const updateRequest = Forms.newRequest().setUpdateItem({
72
- item: {
73
- itemId: this.getId(),
74
- questionItem: {
75
- question: {
76
- choiceQuestion: {
77
- options: newOptions,
78
- },
79
- },
80
- },
81
- },
82
- location: {
83
- index: currentIndex,
84
- },
85
- updateMask: 'questionItem.question.choiceQuestion.options',
86
- });
87
-
88
- const batchRequest = Forms.newBatchUpdateFormRequest().setRequests([updateRequest]);
89
- Forms.Form.batchUpdate(batchRequest, this.__form.getId());
90
-
91
- return this;
92
- }
93
-
94
- /**
95
- * Sets whether this item is required.
96
- * @param {boolean} enabled Whether the item is required.
97
- * @returns {FakeCheckboxItem} This item, for chaining.
98
- */
99
- setRequired(enabled) {
100
- const { nargs, matchThrow } = signatureArgs(arguments, 'CheckboxItem.setRequired');
101
- if (nargs !== 1 || !is.boolean(enabled)) {
102
- matchThrow('Invalid arguments: expected a boolean.');
103
- }
104
-
105
- const currentIndex = this._getCurrentIndex();
106
-
107
- const updateRequest = Forms.newRequest().setUpdateItem({
108
- item: {
109
- itemId: this.getId(),
110
- questionItem: {
111
- question: {
112
- required: enabled,
113
- },
114
- },
115
- },
116
- location: {
117
- index: currentIndex,
118
- },
119
- updateMask: 'questionItem.question.required',
120
- });
121
-
122
- const batchRequest = Forms.newBatchUpdateFormRequest().setRequests([updateRequest]);
123
- Forms.Form.batchUpdate(batchRequest, this.__form.getId());
124
-
125
- return this;
126
- }
127
-
128
- /**
129
- * Returns whether this item is required.
130
- * @returns {boolean} True if the item is required, false otherwise.
131
- */
132
- isRequired() {
133
- return this.__resource.questionItem?.question?.required || false;
134
- }
135
-
136
28
  toString() {
137
29
  return 'CheckboxItem';
138
30
  }
@@ -1,4 +1,6 @@
1
1
  import { Proxies } from '../../support/proxies.js';
2
+ import { PageNavigationType, ItemType } from '../enums/formsenums.js';
3
+ import { newFakePageBreakItem } from './fakepagebreakitem.js';
2
4
 
3
5
  export const newFakeChoice = (...args) => {
4
6
  return Proxies.guard(new FakeChoice(...args));
@@ -10,33 +12,48 @@ export const newFakeChoice = (...args) => {
10
12
  * @see https://developers.google.com/apps-script/reference/forms/choice
11
13
  */
12
14
  export class FakeChoice {
13
- /**
14
- * @param {string} value The value of the choice.
15
- * @param {boolean} isCorrect Whether the choice is correct.
16
- * @param {import('./pagebreakitem.js').FakePageBreakItem} page The page to navigate to.
17
- * @param {FormApp.PageNavigationType} navigationType The navigation type.
18
- */
19
- constructor(value, isCorrect, page, navigationType) {
15
+ constructor(value, navType, pageId, form, parentItemType) {
20
16
  this.__value = value;
21
- this.__isCorrect = isCorrect;
22
- this.__page = page;
23
- this.__navigationType = navigationType;
24
- }
25
-
26
- getGotoPage() {
27
- return this.__page;
17
+ this.__navType = navType;
18
+ this.__pageId = pageId;
19
+ this.__form = form;
20
+ this.__parentItemType = parentItemType;
28
21
  }
29
22
 
30
- getIsCorrect() {
31
- return this.__isCorrect;
23
+ getValue() {
24
+ return this.__value;
32
25
  }
33
26
 
27
+ /**
28
+ * Gets the page navigation type for this choice.
29
+ * @returns {import('../enums/formsenums.js').PageNavigationType | null} The page navigation type, or null if no navigation is set.
30
+ */
34
31
  getPageNavigationType() {
35
- return this.__navigationType;
32
+ if (this.__navType) {
33
+ // The navType is stored as a string like 'goToPage' or 'CONTINUE'.
34
+ // We need to return the corresponding enum value.
35
+ const navEnum = PageNavigationType[this.__navType.toUpperCase()];
36
+ return navEnum || null;
37
+ }
38
+ return null;
36
39
  }
37
40
 
38
- getValue() {
39
- return this.__value;
41
+ /**
42
+ * Gets the page-break item that this choice navigates to.
43
+ * @returns {import('./fakepagebreakitem.js').FakePageBreakItem | null} The page-break item, or null if the choice does not navigate to a page.
44
+ */
45
+ getGoToPage() {
46
+ // Per live documentation, this method only applies to choices from a MultipleChoiceItem.
47
+ if (this.__parentItemType !== ItemType.MULTIPLE_CHOICE) {
48
+ throw new TypeError('getGoToPage is not a function');
49
+ }
50
+
51
+ if (this.__navType === 'GO_TO_PAGE' && this.__pageId && this.__form) {
52
+ // We need the form context to find the item by its ID.
53
+ const item = this.__form.getItemById(this.__pageId);
54
+ return item ? item.asPageBreakItem() : null;
55
+ }
56
+ return null;
40
57
  }
41
58
 
42
59
  toString() {
@@ -0,0 +1,67 @@
1
+ import { Proxies } from '../../support/proxies.js';
2
+ import { newFakeFormItem } from './fakeformitem.js';
3
+ import { newFakeChoice } from './fakechoice.js';
4
+
5
+ export const newFakeChoiceItem = (form, itemId) => {
6
+ return Proxies.guard(new FakeChoiceItem(form, itemId));
7
+ };
8
+
9
+ /**
10
+ * @class FakeChoiceItem
11
+ * A base class for items that have a list of choices, like MultipleChoiceItem and CheckboxItem.
12
+ * It is not a real Apps Script class but a helper for the fake environment.
13
+ */
14
+ export class FakeChoiceItem extends newFakeFormItem().constructor {
15
+ constructor(form, itemId) {
16
+ super(form, itemId);
17
+ }
18
+
19
+ /**
20
+ * Creates a new choice.
21
+ * @param {string} value The text for the new choice.
22
+ * @returns {import('./fakechoice.js').FakeChoice} The new choice.
23
+ */
24
+ createChoice(value) {
25
+ return newFakeChoice(value);
26
+ }
27
+
28
+ /**
29
+ * Gets the choices for this item.
30
+ * @returns {import('./fakechoice.js').FakeChoice[]} An array of the choices.
31
+ */
32
+ getChoices() {
33
+ const choiceQuestion = this.__resource.questionItem.question.choiceQuestion;
34
+ return choiceQuestion.options.map(option => newFakeChoice(option.value));
35
+ }
36
+
37
+ /**
38
+ * Sets the choices for this item.
39
+ * @param {import('./fakechoice.js').FakeChoice[]} choices The new choices.
40
+ * @returns {FakeChoiceItem} The item, for chaining.
41
+ */
42
+ setChoices(choices) {
43
+ const choiceQuestion = this.__resource.questionItem.question.choiceQuestion;
44
+ choiceQuestion.options = choices.map(choice => ({ value: choice.getValue() }));
45
+
46
+ const updateRequest = Forms.newRequest().setUpdateItem({
47
+ item: {
48
+ itemId: this.getId(),
49
+ questionItem: {
50
+ question: {
51
+ choiceQuestion: choiceQuestion,
52
+ },
53
+ },
54
+ },
55
+ location: {
56
+ index: this.getIndex(),
57
+ },
58
+ updateMask: 'questionItem.question.choiceQuestion',
59
+ });
60
+
61
+ return this.__update(updateRequest);
62
+ }
63
+
64
+ toString() {
65
+ return 'FakeChoiceItem';
66
+ }
67
+ }