@mcpher/gas-fakes 1.2.20 → 1.2.21
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 +4 -0
- package/.claspignore +6 -0
- package/ex-logo.png +0 -0
- package/logo.png +0 -0
- package/package.json +2 -1
- package/run.js +35 -0
- package/src/Code.js +3 -0
- package/src/appsscript.json +5 -0
- package/src/services/advforms/fakeadvforms.js +2 -1
- package/src/services/advforms/fakeadvformsform.js +12 -0
- package/src/services/advforms/fakeresponses.js +30 -0
- package/src/services/formapp/fakecheckboxgriditem.js +105 -0
- package/src/services/formapp/fakecheckboxitem.js +2 -110
- package/src/services/formapp/fakechoice.js +36 -19
- package/src/services/formapp/fakechoiceitem.js +67 -0
- package/src/services/formapp/fakeform.js +332 -17
- package/src/services/formapp/fakeformapp.js +1 -1
- package/src/services/formapp/fakeformitem.js +62 -10
- package/src/services/formapp/fakeformresponse.js +84 -0
- package/src/services/formapp/fakegriditem.js +105 -0
- package/src/services/formapp/fakeitemresponse.js +56 -0
- package/src/services/formapp/fakelistitem.js +124 -0
- package/src/services/formapp/fakemultiplechoiceitem.js +25 -0
- package/src/services/formapp/fakepagebreakitem.js +54 -0
- package/src/services/formapp/fakescaleitem.js +121 -0
- package/src/services/formapp/fakesectionheaderitem.js +25 -0
- package/src/services/formapp/faketextitem.js +20 -0
- package/src/services/formapp/formitems.js +6 -1
- package/src/support/sxforms.js +8 -4
- package/src/support/syncit.js +2 -2
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { Proxies } from '../../support/proxies.js';
|
|
2
2
|
import { newFakeFormItem } from './fakeformitem.js';
|
|
3
|
+
import { newFakeFormResponse } from './fakeformresponse.js';
|
|
4
|
+
import { newFakeGridItem } from './fakegriditem.js';
|
|
5
|
+
import { newFakeCheckboxGridItem } from './fakecheckboxgriditem.js';
|
|
6
|
+
import { newFakeSectionHeaderItem } from './fakesectionheaderitem.js';
|
|
7
|
+
import { newFakeScaleItem } from './fakescaleitem.js';
|
|
3
8
|
import './formitems.js'; // Import for side effects (item class registration)
|
|
9
|
+
import { newFakeMultipleChoiceItem } from './fakemultiplechoiceitem.js';
|
|
4
10
|
import { newFakeCheckboxItem } from './fakecheckboxitem.js';
|
|
11
|
+
import { newFakeListItem } from './fakelistitem.js';
|
|
12
|
+
import { newFakePageBreakItem } from './fakepagebreakitem.js';
|
|
13
|
+
import { newFakeTextItem } from './faketextitem.js';
|
|
5
14
|
|
|
6
15
|
export const newFakeForm = (...args) => {
|
|
7
16
|
return Proxies.guard(new FakeForm(...args));
|
|
@@ -17,19 +26,22 @@ export class FakeForm {
|
|
|
17
26
|
* @param {object} resource the form resource from Forms API
|
|
18
27
|
*/
|
|
19
28
|
constructor(resource) {
|
|
29
|
+
// Store the resource provided at creation as the single source of truth for this instance.
|
|
20
30
|
this.__id = resource.formId;
|
|
21
31
|
this.__file = DriveApp.getFileById(this.__id);
|
|
32
|
+
// Since the API doesn't allow setting the published state, we'll manage it internally for the fake.
|
|
33
|
+
// A new form defaults to accepting responses (true).
|
|
34
|
+
this.__publishedState = resource.settings?.state !== 'INACTIVE';
|
|
22
35
|
}
|
|
23
36
|
|
|
24
37
|
get __resource() {
|
|
25
38
|
return Forms.Form.get(this.__id);
|
|
26
39
|
}
|
|
27
|
-
|
|
28
40
|
saveAndClose() {
|
|
29
41
|
// this is a no-op in fake environment since it is stateless
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
|
|
44
|
+
__addItem(itemResource, itemFactory) {
|
|
33
45
|
const createRequest = Forms.newRequest().setCreateItem({
|
|
34
46
|
item: itemResource,
|
|
35
47
|
location: {
|
|
@@ -62,16 +74,178 @@ export class FakeForm {
|
|
|
62
74
|
question: {
|
|
63
75
|
choiceQuestion: {
|
|
64
76
|
type: 'CHECKBOX',
|
|
65
|
-
// The API requires at least one
|
|
66
|
-
// We'll emulate by creating a default "Option 1" in the fake environment.
|
|
77
|
+
// The API requires at least one option on creation.
|
|
67
78
|
options: [{ value: 'Option 1' }],
|
|
68
79
|
},
|
|
69
80
|
},
|
|
70
81
|
},
|
|
71
82
|
};
|
|
72
|
-
return this.
|
|
83
|
+
return this.__addItem(itemResource, newFakeCheckboxItem);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Appends a new question item, presented as a grid of columns and rows, that allows the
|
|
88
|
+
* respondent to select one choice per row from a sequence of radio buttons.
|
|
89
|
+
* @returns {import('./fakegriditem.js').FakeGridItem} The new grid item.
|
|
90
|
+
*/
|
|
91
|
+
addGridItem() {
|
|
92
|
+
const itemResource = {
|
|
93
|
+
questionGroupItem: {
|
|
94
|
+
grid: {
|
|
95
|
+
columns: {
|
|
96
|
+
type: 'RADIO',
|
|
97
|
+
// The API requires at least one column
|
|
98
|
+
options: [{ value: 'Column 1' }],
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
// and at least one row
|
|
102
|
+
questions: [
|
|
103
|
+
{
|
|
104
|
+
rowQuestion: {
|
|
105
|
+
title: 'Row 1',
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
return this.__addItem(itemResource, newFakeGridItem);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Appends a new question item, presented as a grid of columns and rows, that allows the
|
|
116
|
+
* respondent to select multiple choices per row from a sequence of checkboxes.
|
|
117
|
+
* @returns {import('./fakecheckboxgriditem.js').FakeCheckboxGridItem} The new checkbox grid item.
|
|
118
|
+
*/
|
|
119
|
+
addCheckboxGridItem() {
|
|
120
|
+
const itemResource = {
|
|
121
|
+
questionGroupItem: {
|
|
122
|
+
grid: {
|
|
123
|
+
columns: {
|
|
124
|
+
type: 'CHECKBOX',
|
|
125
|
+
// The API requires at least one column
|
|
126
|
+
options: [{ value: 'Column 1' }],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
// and at least one row
|
|
130
|
+
questions: [
|
|
131
|
+
{
|
|
132
|
+
rowQuestion: {
|
|
133
|
+
title: 'Row 1',
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
// Note: This will require a newFakeCheckboxGridItem factory.
|
|
140
|
+
return this.__addItem(itemResource, newFakeCheckboxGridItem);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Appends a new layout item that marks the beginning of a new page in the form.
|
|
145
|
+
* @returns {import('./fakepagebreakitem.js').FakePageBreakItem} The new page-break item.
|
|
146
|
+
*/
|
|
147
|
+
addPageBreakItem() {
|
|
148
|
+
const itemResource = {
|
|
149
|
+
pageBreakItem: {},
|
|
150
|
+
};
|
|
151
|
+
return this.__addItem(itemResource, newFakePageBreakItem);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Appends a new question item that allows the respondent to choose one option
|
|
157
|
+
* from a drop-down list.
|
|
158
|
+
* @returns {import('./fakelistitem.js').FakeListItem} The new list item.
|
|
159
|
+
*/
|
|
160
|
+
addListItem() {
|
|
161
|
+
const itemResource = {
|
|
162
|
+
questionItem: {
|
|
163
|
+
question: {
|
|
164
|
+
choiceQuestion: {
|
|
165
|
+
type: 'DROP_DOWN',
|
|
166
|
+
// The API requires at least one option on creation.
|
|
167
|
+
options: [{ value: 'Option 1' }],
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
return this.__addItem(itemResource, newFakeListItem);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Appends a new question item that allows the respondent to choose one option
|
|
178
|
+
* from a list of choices.
|
|
179
|
+
* @returns {import('./fakemultiplechoiceitem.js').FakeMultipleChoiceItem} The new multiple choice item.
|
|
180
|
+
*/
|
|
181
|
+
addMultipleChoiceItem() {
|
|
182
|
+
const itemResource = {
|
|
183
|
+
questionItem: {
|
|
184
|
+
question: {
|
|
185
|
+
choiceQuestion: {
|
|
186
|
+
type: 'RADIO',
|
|
187
|
+
// The API requires at least one option on creation.
|
|
188
|
+
options: [{ value: 'Option 1' }],
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
return this.__addItem(itemResource, newFakeMultipleChoiceItem);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Appends a new layout item that visually indicates the start of a section.
|
|
198
|
+
* @returns {import('./fakesectionheaderitem.js').FakeSectionHeaderItem} The new section header item.
|
|
199
|
+
*/
|
|
200
|
+
addSectionHeaderItem() {
|
|
201
|
+
const itemResource = {
|
|
202
|
+
// The API resource for a section header is an item with a title and description.
|
|
203
|
+
// It is identified by the presence of the `textItem` property.
|
|
204
|
+
title: '',
|
|
205
|
+
textItem: {},
|
|
206
|
+
};
|
|
207
|
+
return this.__addItem(itemResource, newFakeSectionHeaderItem);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Appends a new question item that allows the respondent to choose one option
|
|
212
|
+
* from a numbered sequence of radio buttons.
|
|
213
|
+
* @returns {import('./fakescaleitem.js').FakeScaleItem} The new scale item.
|
|
214
|
+
*/
|
|
215
|
+
addScaleItem() {
|
|
216
|
+
const itemResource = {
|
|
217
|
+
questionItem: {
|
|
218
|
+
question: {
|
|
219
|
+
scaleQuestion: {
|
|
220
|
+
low: 1,
|
|
221
|
+
high: 5,
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
return this.__addItem(itemResource, newFakeScaleItem);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Appends a new question item that allows the respondent to enter a single
|
|
231
|
+
* line of text.
|
|
232
|
+
* @returns {import('./faketextitem.js').FakeTextItem} The new text item.
|
|
233
|
+
*/
|
|
234
|
+
addTextItem() {
|
|
235
|
+
const itemResource = {
|
|
236
|
+
questionItem: {
|
|
237
|
+
question: {
|
|
238
|
+
textQuestion: {
|
|
239
|
+
paragraph: false, // false for short-answer
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
return this.__addItem(itemResource, newFakeTextItem);
|
|
73
245
|
}
|
|
74
246
|
|
|
247
|
+
|
|
248
|
+
|
|
75
249
|
/**
|
|
76
250
|
* Gets the ID of the form.
|
|
77
251
|
* @returns {string} The form ID.
|
|
@@ -89,10 +263,17 @@ export class FakeForm {
|
|
|
89
263
|
if (!this.__resource.items) {
|
|
90
264
|
return null;
|
|
91
265
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
266
|
+
const isKnownItem = (id, item) => {
|
|
267
|
+
if (item.itemId === id) return true;
|
|
268
|
+
if (item.questionItem?.question?.questionId === id) return true;
|
|
269
|
+
const qgroup = item.questionGroupItem?.questions
|
|
270
|
+
if (!qgroup) return false
|
|
271
|
+
const found = qgroup.some(q => q.questionId === id)
|
|
272
|
+
return found
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const itemResource = this.__resource.items.find((item) => isKnownItem(id, item));
|
|
276
|
+
|
|
96
277
|
if (!itemResource) {
|
|
97
278
|
return null;
|
|
98
279
|
}
|
|
@@ -101,10 +282,19 @@ export class FakeForm {
|
|
|
101
282
|
|
|
102
283
|
/**
|
|
103
284
|
* Gets all items in the form.
|
|
285
|
+
* @param {import('../enums/formsenums.js').ItemType} [itemType] If provided, only items of this type are returned.
|
|
104
286
|
* @returns {import('./fakeformitem.js').FakeFormItem[]} An array of all items in the form.
|
|
105
287
|
*/
|
|
106
|
-
getItems() {
|
|
107
|
-
|
|
288
|
+
getItems(itemType) {
|
|
289
|
+
const allItems = this.__resource.items?.map((item) => newFakeFormItem(this, item.itemId)) || [];
|
|
290
|
+
|
|
291
|
+
if (itemType) {
|
|
292
|
+
// The itemType from the enum will be an object, so we compare its string representation
|
|
293
|
+
// against the string representation of the item's type.
|
|
294
|
+
return allItems.filter(item => item.getType().toString() === itemType.toString());
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return allItems;
|
|
108
298
|
}
|
|
109
299
|
|
|
110
300
|
/**
|
|
@@ -114,7 +304,13 @@ export class FakeForm {
|
|
|
114
304
|
getTitle() {
|
|
115
305
|
return this.__resource.info.title;
|
|
116
306
|
}
|
|
117
|
-
|
|
307
|
+
/**
|
|
308
|
+
* Gets the title of the form.
|
|
309
|
+
* @returns {string} The form title.
|
|
310
|
+
*/
|
|
311
|
+
getDescription() {
|
|
312
|
+
return this.__resource.info.description;
|
|
313
|
+
}
|
|
118
314
|
/**
|
|
119
315
|
* Gets the name of the form file in Google Drive.
|
|
120
316
|
* @returns {string} The file name.
|
|
@@ -123,6 +319,34 @@ export class FakeForm {
|
|
|
123
319
|
return this.__file.getName();
|
|
124
320
|
}
|
|
125
321
|
|
|
322
|
+
/**
|
|
323
|
+
* Gets all of the form's responses.
|
|
324
|
+
* @returns {import('./fakeformresponse.js').FakeFormResponse[]} An array of form responses.
|
|
325
|
+
*/
|
|
326
|
+
getResponses() {
|
|
327
|
+
// The advanced Forms service is needed here, but it's an implementation detail of the fake.
|
|
328
|
+
if (!Forms.Form?.Responses) {
|
|
329
|
+
throw new Error(
|
|
330
|
+
'The faked Advanced Forms Service (Forms.Form.Responses) must be available to use getResponses().'
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
const responseList = Forms.Form.Responses.list(this.getId());
|
|
334
|
+
const responses = responseList.responses?.map((r) => newFakeFormResponse(this, r)) || [];
|
|
335
|
+
|
|
336
|
+
// The live Apps Script getResponses() method returns responses in chronological order.
|
|
337
|
+
// The API returns them in reverse chronological order, so we must sort them.
|
|
338
|
+
return responses.sort((a, b) => a.getTimestamp() - b.getTimestamp());
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Gets whether the form is accepting responses.
|
|
343
|
+
* @returns {boolean} true if the form is accepting responses; false otherwise.
|
|
344
|
+
*/
|
|
345
|
+
isPublished() {
|
|
346
|
+
// ACTIVE is the state for accepting responses. The default if not set is ACTIVE.
|
|
347
|
+
return this.__publishedState;
|
|
348
|
+
}
|
|
349
|
+
|
|
126
350
|
/**
|
|
127
351
|
* Sets the name of the form file in Google Drive.
|
|
128
352
|
* @param {string} name The new file name.
|
|
@@ -133,6 +357,79 @@ export class FakeForm {
|
|
|
133
357
|
return this;
|
|
134
358
|
}
|
|
135
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Deletes the item at the given index.
|
|
362
|
+
* @param {import('./fakeformitem.js').FakeFormItem | Integer} itemOrIndex The item to delete, or its 0-indexed position.
|
|
363
|
+
* @returns {FakeForm} The form, for chaining.
|
|
364
|
+
*/
|
|
365
|
+
deleteItem(itemOrIndex) {
|
|
366
|
+
let indexToDelete
|
|
367
|
+
if (typeof itemOrIndex === 'number') {
|
|
368
|
+
indexToDelete = itemOrIndex
|
|
369
|
+
} else if (typeof itemOrIndex === 'object' && typeof itemOrIndex.getIndex === 'function') {
|
|
370
|
+
// It's an Item object, get its index.
|
|
371
|
+
indexToDelete = itemOrIndex.getIndex()
|
|
372
|
+
} else {
|
|
373
|
+
// This handles the case where an invalid object is passed, which can happen during development.
|
|
374
|
+
// The error from the API ("Starting an object on a scalar field") is because the fake was
|
|
375
|
+
// passing the whole object into the batchUpdate request instead of an index.
|
|
376
|
+
// By handling the object case properly, we now get the correct behavior.
|
|
377
|
+
throw new Error(
|
|
378
|
+
`The parameters (${typeof itemOrIndex}) don't match the method signature for FormApp.Form.deleteItem.`
|
|
379
|
+
)
|
|
380
|
+
}
|
|
381
|
+
const deleteRequest = Forms.newRequest().setDeleteItem({
|
|
382
|
+
location: { index: indexToDelete },
|
|
383
|
+
});
|
|
384
|
+
return this.__update(deleteRequest);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Moves the given form item to the specified index.
|
|
389
|
+
* @param {import('./fakeformitem.js').FakeFormItem | Integer} itemOrFrom The item to move, or its 0-indexed position.
|
|
390
|
+
* @param {Integer} toIndex The 0-indexed position to move the item to.
|
|
391
|
+
* @returns {import('./fakeformitem.js').FakeFormItem} The moved item.
|
|
392
|
+
*/
|
|
393
|
+
moveItem(itemOrFrom, toIndex) {
|
|
394
|
+
let fromIndex;
|
|
395
|
+
if (typeof itemOrFrom === 'number') {
|
|
396
|
+
fromIndex = itemOrFrom;
|
|
397
|
+
} else if (typeof itemOrFrom === 'object' && typeof itemOrFrom.getIndex === 'function') {
|
|
398
|
+
fromIndex = itemOrFrom.getIndex();
|
|
399
|
+
} else {
|
|
400
|
+
throw new Error(`The parameters (${typeof itemOrFrom},number) don't match the method signature for FormApp.Form.moveItem.`);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const items = this.getItems();
|
|
404
|
+
if (fromIndex < 0 || fromIndex >= items.length) {
|
|
405
|
+
throw new Error(`The starting position ${fromIndex} is out of bounds.`);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// The item to return is the one at the original 'from' index.
|
|
409
|
+
const itemToMove = items[fromIndex];
|
|
410
|
+
const moveRequest = Forms.newRequest().setMoveItem({
|
|
411
|
+
originalLocation: { index: fromIndex },
|
|
412
|
+
newLocation: { index: toIndex },
|
|
413
|
+
});
|
|
414
|
+
this.__update(moveRequest);
|
|
415
|
+
return itemToMove;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Sets whether the form is accepting responses.
|
|
419
|
+
* @param {boolean} enabled true if the form should accept responses; false otherwise.
|
|
420
|
+
* @returns {FakeForm} The form, for chaining.
|
|
421
|
+
*/
|
|
422
|
+
setPublished(enabled) {
|
|
423
|
+
throw new Error('setPublished is not yet implemented in the fake environment.');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
__update(updateRequest) {
|
|
427
|
+
const batchRequest = Forms.newBatchUpdateFormRequest()
|
|
428
|
+
.setRequests([updateRequest])
|
|
429
|
+
Forms.Form.batchUpdate(batchRequest, this.getId());
|
|
430
|
+
return this;
|
|
431
|
+
}
|
|
432
|
+
|
|
136
433
|
/**
|
|
137
434
|
* Sets the title of the form.
|
|
138
435
|
* @param {string} title The new title for the form.
|
|
@@ -145,12 +442,22 @@ export class FakeForm {
|
|
|
145
442
|
.setInfo(updateInfo)
|
|
146
443
|
.setUpdateMask("title")
|
|
147
444
|
);
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
Forms.Form.batchUpdate(batchRequest, this.getId());
|
|
445
|
+
return this.__update(updateRequest)
|
|
446
|
+
}
|
|
152
447
|
|
|
153
|
-
|
|
448
|
+
/**
|
|
449
|
+
* sets the description of the form.
|
|
450
|
+
* @param {string} description The new title for the form.
|
|
451
|
+
* @returns {FakeForm} The form, for chaining.
|
|
452
|
+
*/
|
|
453
|
+
setDescription(description) {
|
|
454
|
+
const updateInfo = Forms.newFormInfo().setDescription(description);
|
|
455
|
+
const updateRequest = Forms.newRequest().setUpdateFormInfo(
|
|
456
|
+
Forms.newUpdateFormInfoRequest()
|
|
457
|
+
.setInfo(updateInfo)
|
|
458
|
+
.setUpdateMask("description")
|
|
459
|
+
);
|
|
460
|
+
return this.__update(updateRequest)
|
|
154
461
|
}
|
|
155
462
|
|
|
156
463
|
/**
|
|
@@ -161,6 +468,14 @@ export class FakeForm {
|
|
|
161
468
|
return `https://docs.google.com/forms/d/${this.getId()}/edit`;
|
|
162
469
|
}
|
|
163
470
|
|
|
471
|
+
/**
|
|
472
|
+
* Gets the URL to respond to the form.
|
|
473
|
+
* @returns {string} The form URL.
|
|
474
|
+
*/
|
|
475
|
+
getPublishedUrl() {
|
|
476
|
+
return `https://docs.google.com/forms/d/e/${this.getId()}/viewform`;
|
|
477
|
+
}
|
|
478
|
+
|
|
164
479
|
toString() {
|
|
165
480
|
return 'Form';
|
|
166
481
|
}
|
|
@@ -61,7 +61,7 @@ export class FakeFormItem {
|
|
|
61
61
|
asGridItem() { return this.__cast(ItemType.GRID); }
|
|
62
62
|
asImageItem() { return this.__cast(ItemType.IMAGE); }
|
|
63
63
|
asListItem() { return this.__cast(ItemType.LIST); }
|
|
64
|
-
asMultipleChoiceItem() { return this.__cast(ItemType.MULTIPLE_CHOICE); }
|
|
64
|
+
asMultipleChoiceItem() { return this.__cast(ItemType.MULTIPLE_CHOICE, true); }
|
|
65
65
|
asPageBreakItem() { return this.__cast(ItemType.PAGE_BREAK); }
|
|
66
66
|
asParagraphTextItem() { return this.__cast(ItemType.PARAGRAPH_TEXT); }
|
|
67
67
|
asScaleItem() { return this.__cast(ItemType.SCALE); }
|
|
@@ -114,7 +114,8 @@ export class FakeFormItem {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
getId() {
|
|
117
|
-
|
|
117
|
+
// Live Apps Script returns IDs as decimal numbers, not the hex strings from the API.
|
|
118
|
+
return this.__itemId;
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
getIndex() {
|
|
@@ -148,7 +149,8 @@ export class FakeFormItem {
|
|
|
148
149
|
if (question.scaleQuestion) return ItemType.SCALE;
|
|
149
150
|
if (question.rowQuestion) return ItemType.GRID;
|
|
150
151
|
} else if (item.questionGroupItem) {
|
|
151
|
-
|
|
152
|
+
// For GRID and CHECKBOX_GRID, columns is an object, not an array.
|
|
153
|
+
const gridType = item.questionGroupItem.grid.columns.type;
|
|
152
154
|
return gridType === 'RADIO' ? ItemType.GRID : ItemType.CHECKBOX_GRID;
|
|
153
155
|
} else if (item.pageBreakItem) {
|
|
154
156
|
return ItemType.PAGE_BREAK;
|
|
@@ -161,6 +163,12 @@ export class FakeFormItem {
|
|
|
161
163
|
}
|
|
162
164
|
throw new Error(`Unknown item type for resource: ${JSON.stringify(item)}`);
|
|
163
165
|
}
|
|
166
|
+
__update(updateRequest) {
|
|
167
|
+
const response = this.__form.__update(updateRequest);
|
|
168
|
+
// The local resource is now stale, so we need to update it.
|
|
169
|
+
// The response from batchUpdate contains the updated form resource.
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
164
172
|
|
|
165
173
|
setHelpText(text) {
|
|
166
174
|
const { nargs, matchThrow } = signatureArgs(arguments, 'Item.setHelpText');
|
|
@@ -181,10 +189,7 @@ export class FakeFormItem {
|
|
|
181
189
|
// Crucially, only ask the API to update the description field.
|
|
182
190
|
updateMask: 'description',
|
|
183
191
|
});
|
|
184
|
-
|
|
185
|
-
const batchRequest = Forms.newBatchUpdateFormRequest().setRequests([updateRequest]);
|
|
186
|
-
Forms.Form.batchUpdate(batchRequest, this.__form.getId());
|
|
187
|
-
return this;
|
|
192
|
+
return this.__update (updateRequest)
|
|
188
193
|
}
|
|
189
194
|
|
|
190
195
|
setTitle(title) {
|
|
@@ -207,9 +212,56 @@ export class FakeFormItem {
|
|
|
207
212
|
updateMask: 'title',
|
|
208
213
|
});
|
|
209
214
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
215
|
+
return this.__update (updateRequest)
|
|
216
|
+
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Determines whether the respondent must answer the question.
|
|
221
|
+
* @returns {boolean} whether the respondent must answer the question
|
|
222
|
+
*/
|
|
223
|
+
isRequired() {
|
|
224
|
+
if (this.__resource.questionItem) {
|
|
225
|
+
return this.__resource.questionItem.question.required || false;
|
|
226
|
+
} else if (this.__resource.questionGroupItem) {
|
|
227
|
+
return this.__resource.questionGroupItem.questions?.some(q => q.required) || false;
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Sets whether the respondent must answer the question.
|
|
234
|
+
* @param {boolean} enabled
|
|
235
|
+
* @returns {FakeFormItem} the current item (for chaining)
|
|
236
|
+
*/
|
|
237
|
+
setRequired(enabled) {
|
|
238
|
+
const { nargs, matchThrow } = signatureArgs(arguments, 'Item.setRequired');
|
|
239
|
+
if (nargs !== 1 || !is.boolean(enabled)) matchThrow('Invalid arguments');
|
|
240
|
+
|
|
241
|
+
let updateMask;
|
|
242
|
+
const updatedResource = JSON.parse(JSON.stringify(this.__resource));
|
|
243
|
+
|
|
244
|
+
if (updatedResource.questionItem) {
|
|
245
|
+
updatedResource.questionItem.question.required = enabled;
|
|
246
|
+
updateMask = 'questionItem.question.required';
|
|
247
|
+
} else if (updatedResource.questionGroupItem) {
|
|
248
|
+
if (updatedResource.questionGroupItem.questions) {
|
|
249
|
+
updatedResource.questionGroupItem.questions.forEach(q => {
|
|
250
|
+
q.required = enabled;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
updateMask = 'questionGroupItem.questions';
|
|
254
|
+
} else {
|
|
255
|
+
throw new Error('This item type does not support setRequired.');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const updateRequest = Forms.newRequest().setUpdateItem({
|
|
259
|
+
item: updatedResource,
|
|
260
|
+
location: { index: this.getIndex() },
|
|
261
|
+
updateMask: updateMask,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return this.__update (updateRequest)
|
|
213
265
|
}
|
|
214
266
|
|
|
215
267
|
toString() {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Proxies } from '../../support/proxies.js';
|
|
2
|
+
import { newFakeItemResponse } from './fakeitemresponse.js';
|
|
3
|
+
|
|
4
|
+
export const newFakeFormResponse = (...args) => {
|
|
5
|
+
return Proxies.guard(new FakeFormResponse(...args));
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @class FakeFormResponse
|
|
10
|
+
* @see https://developers.google.com/apps-script/reference/forms/form-response
|
|
11
|
+
*/
|
|
12
|
+
export class FakeFormResponse {
|
|
13
|
+
/**
|
|
14
|
+
*
|
|
15
|
+
* @param {import('./fakeform.js').FakeForm} form the parent form
|
|
16
|
+
* @param {object} resource the response resource from the Forms API
|
|
17
|
+
*/
|
|
18
|
+
constructor(form, resource) {
|
|
19
|
+
this.__form = form;
|
|
20
|
+
this.__resource = resource;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gets the email address of the respondent.
|
|
25
|
+
* @returns {string} the respondent's email
|
|
26
|
+
*/
|
|
27
|
+
getRespondentEmail() {
|
|
28
|
+
return this.__resource.respondentEmail || '';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Gets the unique ID for this form response.
|
|
33
|
+
* @returns {string} the unique ID
|
|
34
|
+
*/
|
|
35
|
+
getId() {
|
|
36
|
+
return this.__resource.responseId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Gets the date and time the response was submitted.
|
|
41
|
+
* @returns {Date} the submission timestamp
|
|
42
|
+
*/
|
|
43
|
+
getTimestamp() {
|
|
44
|
+
return new Date(this.__resource.lastSubmittedTime);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Gets all item responses contained in the form response.
|
|
49
|
+
* @returns {import('./fakeitemresponse.js').FakeItemResponse[]} an array of item responses
|
|
50
|
+
*/
|
|
51
|
+
getItemResponses() {
|
|
52
|
+
if (!this.__resource.answers) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Create a map to group answers by their parent form item. This is crucial for grid items,
|
|
57
|
+
// where each row is a separate answer in the API response but should be consolidated
|
|
58
|
+
// into a single ItemResponse in Apps Script.
|
|
59
|
+
const groupedAnswers = new Map();
|
|
60
|
+
|
|
61
|
+
for (const [questionId, answer] of Object.entries(this.__resource.answers)) {
|
|
62
|
+
const item = this.__form.getItemById(questionId);
|
|
63
|
+
|
|
64
|
+
if (item) {
|
|
65
|
+
const itemId = item.getId(); // Use the unique item ID as the key.
|
|
66
|
+
if (!groupedAnswers.has(itemId)) {
|
|
67
|
+
// Store the item itself along with an array for its answers.
|
|
68
|
+
groupedAnswers.set(itemId, { item, answers: [] });
|
|
69
|
+
}
|
|
70
|
+
// Add the raw answer object from the API response to the item's answer list.
|
|
71
|
+
// This correctly groups all row answers for a grid under the same parent item.
|
|
72
|
+
groupedAnswers.get(itemId).answers.push(answer);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Now, create one FakeItemResponse for each grouped item.
|
|
77
|
+
const itemResponses = Array.from(groupedAnswers.values()).map(({ item, answers }) => {
|
|
78
|
+
return newFakeItemResponse(item, answers);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Finally, sort the responses based on the item's index in the form.
|
|
82
|
+
return itemResponses.sort((a, b) => a.getItem().getIndex() - b.getItem().getIndex());
|
|
83
|
+
}
|
|
84
|
+
}
|