Skip to content

Instantly share code, notes, and snippets.

@cburgmer
Last active November 11, 2017 17:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cburgmer/71ee75165ae3ee5ff854f65a0615e70a to your computer and use it in GitHub Desktop.
Save cburgmer/71ee75165ae3ee5ff854f65a0615e70a to your computer and use it in GitHub Desktop.
Mock Document for Google Apps Scripts, this implements a partial interface of https://developers.google.com/apps-script/reference/document/document
var doc = fakeDocument(
aParagraph('some text'),
aBookmark('my-id')
);
doc.getBody().getNumChildren();
// => 2
doc.getBody().getChild(0).getText();
// => 'some text'
doc.getBookmark('my-id').getId();
// => 'my-id'
doc.getBookmark('my-id').getPosition().getElement().getParent().getType();
// => DocumentApp.ElementType.BODY_SECTION
var aBookmark = function (id) {
return {majorType: 'bookmark', id: id};
};
var aTable = function () {
var children = Array.prototype.slice.call(arguments);
return {majorType: 'element', type: DocumentApp.ElementType.TABLE, children: children};
};
var aTableRow = function () {
var children = Array.prototype.slice.call(arguments);
return {majorType: 'element', type: DocumentApp.ElementType.TABLE_ROW, children: children};
};
var aTableCell = function () {
var children = Array.prototype.slice.call(arguments);
return {majorType: 'element', type: DocumentApp.ElementType.TABLE_CELL, children: children};
};
var aParagraph = function () {
var text = arguments[0];
var children = Array.prototype.slice.call(arguments, 1);
return {majorType: 'element', type: DocumentApp.ElementType.PARAGRAPH, text: text, children: children};
};
var anInlineImage = function () {
return {majorType: 'element', type: DocumentApp.ElementType.INLINE_IMAGE};
};
var fakeDocument = function () {
var childNodeDescriptions = Array.prototype.slice.call(arguments);
var findNodeDescription = function (nodeDescriptions, matcher) {
var i, candidate;
for(i = 0; i < nodeDescriptions.length; i++) {
candidate = nodeDescriptions[i];
if (matcher(candidate)) {
return candidate;
}
// depth first
if (candidate.children) {
candidate = findNodeDescription(candidate.children, matcher);
if (candidate) {
return candidate;
}
}
}
};
var matchBookmark = function (id) {
return function (candidateNodeDescription) {
return candidateNodeDescription.majorType === 'bookmark' && candidateNodeDescription.id === id;
};
};
var elementDescriptions = function (nodeDescriptions) {
return nodeDescriptions.filter(function (nodeDescription) {
return nodeDescription.majorType === 'element';
});
};
var fakeElement = function (description) {
var element = {};
element.getType = function () { return description.type; };
element.getParent = function () { return fakeNode(description.parent); };
element.getPreviousSibling = function () {
var siblings = elementDescriptions(description.parent.children);
var selfIdx = siblings.indexOf(description);
if (selfIdx === 0) {
return;
}
var previousSiblingDescription = siblings[selfIdx - 1];
return fakeNode(previousSiblingDescription);
};
element.removeFromParent = function () {
var siblings = description.parent.children;
var selfIdx = siblings.indexOf(description);
siblings.splice(selfIdx, 1);
};
return element;
};
var fakeContainerElement = function (description) {
var element = fakeElement(description);
element.getNumChildren = function () { return elementDescriptions(description.children).length; };
element.getChild = function (idx) { return fakeNode(elementDescriptions(description.children)[idx]); };
return element;
};
var fakeTableElement = function (description) {
var element = fakeContainerElement(description);
element.getNumRows = function () { return description.children.length; };
element.getRow = function (idx) { return fakeNode(description.children[idx]); };
element.removeRow = function (idx) { description.children.splice(idx, 1) };
return element;
};
var fakeRowElement = function (description) {
var element = fakeContainerElement(description);
element.getCell = function (idx) { return fakeNode(description.children[idx]); };
return element;
};
var fakeCellElement = function (description) {
var element = fakeContainerElement(description);
element.getText = function () {
return elementDescriptions(description.children)
.map(function (descr) { return fakeNode(descr); })
.map(function (node) { return node.getText(); })
.join('\n');
};
return element;
};
var fakeParagraphElement = function (description) {
var element = fakeContainerElement(description);
element.getText = function () { return description.text; };
return element;
};
var fakeBookmark = function (description) {
return {
getId: function () { return description.id; },
getPosition: function () {
var siblings = description.parent.children;
var selfIdx = siblings.indexOf(description);
return {
getElement: function () {
var previousElementSiblings = elementDescriptions(siblings.slice(0, selfIdx));
var nearestPreviousElementSibling = previousElementSiblings[previousElementSiblings.length - 1];
return fakeNode(nearestPreviousElementSibling);
}
};
},
remove: function () {
var siblings = description.parent.children;
var selfIdx = siblings.indexOf(description);
siblings.splice(selfIdx, 1);
}
};
};
var fakeNode = function (description) {
if (description.majorType === 'bookmark') {
return fakeBookmark(description);
} else if (description.type === DocumentApp.ElementType.TABLE) {
return fakeTableElement(description);
} else if (description.type === DocumentApp.ElementType.TABLE_ROW) {
return fakeRowElement(description);
} else if (description.type === DocumentApp.ElementType.TABLE_CELL) {
return fakeCellElement(description);
} else if (description.type === DocumentApp.ElementType.PARAGRAPH) {
return fakeParagraphElement(description);
} else if (description.type === DocumentApp.ElementType.BODY_SECTION) {
return fakeContainerElement(description);
} else {
return fakeElement(description);
}
};
var annotateDescriptionsWithParent = function (nodeDescriptions, parent) {
nodeDescriptions.forEach(function (nodeDescription) {
nodeDescription.parent = parent;
if (nodeDescription.children) {
annotateDescriptionsWithParent(nodeDescription.children, nodeDescription);
};
});
};
var bodyNodeDescription = {type: DocumentApp.ElementType.BODY_SECTION, children: childNodeDescriptions};
annotateDescriptionsWithParent(childNodeDescriptions, bodyNodeDescription);
return {
getBody: function () {
return fakeNode(bodyNodeDescription);
},
getBookmark: function (id) {
var match = findNodeDescription(childNodeDescriptions, matchBookmark(id));
if (match) {
return fakeNode(match);
}
}
};
};
function testFakeDocument() {
var assert = function (value) {
if (!value) {
throw new Error('assert ' + value);
}
};
try {
var emptyDoc = fakeDocument();
assert(emptyDoc.getBody().getType() === DocumentApp.ElementType.BODY_SECTION);
assert(emptyDoc.getBody().getNumChildren() === 0);
var docWithParagraph = fakeDocument(aParagraph('there\'s text'));
assert(docWithParagraph.getBody().getNumChildren() === 1);
assert(docWithParagraph.getBody().getChild(0) !== null);
assert(docWithParagraph.getBody().getChild(0).getType() === DocumentApp.ElementType.PARAGRAPH);
assert(docWithParagraph.getBody().getChild(0).getNumChildren() === 0);
assert(docWithParagraph.getBody().getChild(0).getParent().getType() === DocumentApp.ElementType.BODY_SECTION);
assert(docWithParagraph.getBody().getChild(0).getText() === 'there\'s text');
var twoChildrenDoc = fakeDocument(aParagraph('one'), aParagraph('two'));
assert(twoChildrenDoc.getBody().getNumChildren() === 2);
assert(twoChildrenDoc.getBody().getChild(0) !== twoChildrenDoc.getBody().getChild(1));
assert(twoChildrenDoc.getBody().getChild(0).getText() === 'one');
assert(twoChildrenDoc.getBody().getChild(0).getPreviousSibling() === undefined);
assert(twoChildrenDoc.getBody().getChild(1).getText() === 'two');
assert(twoChildrenDoc.getBody().getChild(1).getPreviousSibling().getText() === 'one');
var someDoc = fakeDocument(aParagraph(''));
someDoc.getBody().getChild(0).removeFromParent();
assert(someDoc.getBody().getNumChildren() === 0);
var anotherDoc = fakeDocument(aParagraph('one'), aParagraph('two'));
var anotherDocsBody = anotherDoc.getBody();
anotherDoc.getBody().getChild(0).removeFromParent();
assert(anotherDoc.getBody().getNumChildren() === 1);
assert(anotherDocsBody.getNumChildren() === 1);
assert(anotherDoc.getBody().getChild(0).getText() === 'two');
assert(anotherDocsBody.getChild(0).getText() === 'two');
var docWithEmptyTable = fakeDocument(aTable());
assert(docWithEmptyTable.getBody().getChild(0).getNumRows() === 0);
var docWithOneRowOneCellTable = fakeDocument(aTable(aTableRow(aTableCell(aParagraph('some text')))));
assert(docWithOneRowOneCellTable.getBody().getChild(0).getType() === DocumentApp.ElementType.TABLE);
assert(docWithOneRowOneCellTable.getBody().getChild(0).getNumRows() === 1);
assert(docWithOneRowOneCellTable.getBody().getChild(0).getRow(0).getType() === DocumentApp.ElementType.TABLE_ROW);
assert(docWithOneRowOneCellTable.getBody().getChild(0).getRow(0).getCell(0).getType() === DocumentApp.ElementType.TABLE_CELL);
assert(docWithOneRowOneCellTable.getBody().getChild(0).getRow(0).getCell(0).getText() === 'some text');
assert(docWithOneRowOneCellTable.getBody().getChild(0).getRow(0).getCell(0).getChild(0).getType() === DocumentApp.ElementType.PARAGRAPH);
assert(docWithOneRowOneCellTable.getBody().getChild(0).getRow(0).getCell(0).getChild(0).getText() === 'some text');
assert(docWithOneRowOneCellTable.getBody().getChild(0).getRow(0).getCell(0).getChild(0).getParent().getType() === DocumentApp.ElementType.TABLE_CELL);
var docWithTwoRowsTable = fakeDocument(aTable(aTableRow(aTableCell(aParagraph('one'))), aTableRow(aTableCell(aParagraph('two')))));
assert(docWithTwoRowsTable.getBody().getChild(0).getNumRows() === 2);
assert(docWithTwoRowsTable.getBody().getChild(0).getRow(1).getCell(0).getText() === 'two');
docWithTwoRowsTable.getBody().getChild(0).removeRow(1);
assert(docWithTwoRowsTable.getBody().getChild(0).getNumRows() === 1);
assert(docWithTwoRowsTable.getBody().getChild(0).getRow(0).getCell(0).getText() === 'one');
var docWithTableCellAndTwoParagraphs = fakeDocument(aTable(aTableRow(aTableCell(aParagraph('some text'), aParagraph('next line')))));
assert(docWithTableCellAndTwoParagraphs.getBody().getChild(0).getRow(0).getCell(0).getText() === 'some text\nnext line');
var docWithTableWithBookmark = fakeDocument(aTable(aTableRow(aTableCell(aParagraph('some text'), aBookmark('cell bookmark')))));
assert(docWithTableWithBookmark.getBookmark('cell bookmark').getPosition().getElement().getText() === 'some text');
var docWithBookmark = fakeDocument(aParagraph(''), aBookmark('some bookmark'));
assert(docWithBookmark.getBody().getNumChildren() === 1);
assert(docWithBookmark.getBody().getChild(0).getNumChildren() === 0);
assert(docWithBookmark.getBookmark('some bookmark').getId() === 'some bookmark');
assert(docWithBookmark.getBookmark('some bookmark').getPosition().getElement().getNumChildren() === 0);
var anotherDocWithBookmark = fakeDocument(aParagraph('one'), aBookmark('bookmark'), aParagraph('two'));
assert(anotherDocWithBookmark.getBody().getNumChildren() === 2);
assert(anotherDocWithBookmark.getBody().getChild(1).getPreviousSibling().getText() === 'one');
var yetAnotherDocWithBookmark = fakeDocument(aParagraph(''), aBookmark('my bookmark'));
yetAnotherDocWithBookmark.getBookmark('my bookmark').remove();
assert(yetAnotherDocWithBookmark.getBookmark('my bookmark') === undefined);
var docWithInlineImage = fakeDocument(anInlineImage());
assert(docWithInlineImage.getBody().getChild(0).getType() === DocumentApp.ElementType.INLINE_IMAGE);
assert(docWithInlineImage.getBody().getChild(0).getParent().getType() === DocumentApp.ElementType.BODY_SECTION);
var anotherDocWithInlineImage = fakeDocument(aParagraph(''), anInlineImage());
assert(anotherDocWithInlineImage.getBody().getChild(1).getPreviousSibling().getType() === DocumentApp.ElementType.PARAGRAPH);
anotherDocWithInlineImage.getBody().getChild(1).removeFromParent();
assert(anotherDocWithInlineImage.getBody().getNumChildren() === 1);
} catch(e) {
// Work around bad UX on failing asserts
Logger.log(e.message);
Logger.log(e.stack);
throw e;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment