Open HarryPi opened 1 year ago
Thanks for reporting this. It looks like there are 3 different issues with the mock package here.
@HarryPi @millerds any workaround for this issue, this is happening for me also getting Error: Property values needs to be present in object model before load is called.
while trying to mock this
const items = [
{
columnIndex: 1,
values: [[""]],
formulas: [[""]],
address: "Sheet1!B1",
},
];
const mock = new OfficeBddinMock.OfficeMockObject({
areas: { items },
});
mock.load("areas/items/values");
mock.sync();
console.log(mock.areas.items[0].values);
@ramzitannous that is not the same thing. It's also incomplete . . . you aren't mocking the whole office-js path. On top of that I think your resulting json structure doesn't actually include an element named "items" . . . you just assigned a variable named items as part of an object named "areas" without giving it a property name in the object.
Look at https://learn.microsoft.com/en-us/office/dev/add-ins/testing/unit-testing for some usage examples.
@millerds this pattern is needed for range area testing for ex. targetRangeArea.load("areas/items/values,areas/items/formulas,areas/items/address");
The original comment about collections is a currently limitation that needs to be addressed. Your error, however, is not the same thing even if you are trying to accomplish the same type of testing. Even if support for collects were there, you would be hitting your error because your mock structure above the collection isn't complete and the collection structure you are creating doesn't match the officejs collection structure (which is what you are trying to mock).
@millerds the structure I'm mocking is exactly like what sheet.getRanges() returns, can you help and show what is wrong in the above code ?
OK . . . looks like I was mistaken. I thought that the way you were building the structure would leave an anonymous reference to the item collection, but apparently the variable name is keep and used at the property name. It's still just a fragment of the full api structure that would be used in the actual code. The error you getting is different because you're trying to load a property of one of the items of the collection which doesn't exist in the mock object created from the data object.
In any case . . . there isn't a workaround. The Mock object doesn't handle collections/arrays and so when parsing out the data input it stops going down at the array and moves onto the next property. This is a limitation of the current mock object. We have a work item on our back log to improve this.
@millerds is there a timeline for implementing this ?
We don't comment on timelines.
It appears that the OfficeMockObject
does not support any property that is an array.
For example:
const mockData = {
context: {
document: {
body: {
fields: {
items: [
{
code: " DATE \\* MERGEFORMAT ",
result: '{"hyperlink":"","isEmpty":false,"style":"Normal","styleBuiltIn":"Normal","text":"23/10/2024"}',
},
{
code: " SECTION \\* MERGEFORMAT ",
result: '{"hyperlink":"","isEmpty":false,"style":"Normal","styleBuiltIn":"Normal","text":"2"}',
}
]
},
},
},
},
run: async function (callback) {
return await callback(this.context)
},
}
const mock = new OfficeMockObject(mockData, OfficeApp.Word)
When the mock object is created, the field
items
indexed values are not themselves created as instances of OfficeMockObject
. So when you look at mock.context.document.body.fields.items[0]
in the debugger you can see it is just a plain object. Where as mock.context.document.body.fields
is an instance of OfficeMockObject
.
A solution that works for me is to use a dictionary like structure. For example:
const mockData = {
context: {
document: {
body: {
fields: {
items: {
0: {
code: " DATE \\* MERGEFORMAT ",
result: '{"hyperlink":"","isEmpty":false,"style":"Normal","styleBuiltIn":"Normal","text":"23/10/2024"}',
},
1: {
code: " SECTION \\* MERGEFORMAT ",
result: '{"hyperlink":"","isEmpty":false,"style":"Normal","styleBuiltIn":"Normal","text":"2"}',
},
length: 2,
},
},
},
},
},
run: async function (callback) {
return await callback(this.context)
},
}
const mock = new OfficeMockObject(mockData, OfficeApp.Word)
Now when accessing mock.context.document.body.fields.items[0]
there will be an instance of OfficeMockObject
with the expected code
and result
keys stored in its private _properties
map.
The next problem is with the example javascript in the documentation, such as this
https://learn.microsoft.com/en-us/javascript/api/word/word.fieldcollection?view=word-js-preview#examples. This code works with the real Office JS API and it will work in a unit test up until fields.load(["code", "result"])
. To be able to access the the fields code
and result
property values with an instance of the OfficeMockObject
you would need to use fields.load(["items/0/code", "items/0/result", "items/1/code", "items/1/result"])
instead.
One solution to the above problem is to "monkey patch" the OfficeMockObject
:
if (!OfficeMockObject.prototype._load) {
OfficeMockObject.prototype._load = OfficeMockObject.prototype.load
OfficeMockObject.prototype.load = function (propertyArgument: string | string[] | ObjectData) {
if (propertyArgument instanceof OfficeMockObject) {
const mock = propertyArgument as OfficeMockObject
return mock._load("*")
} else if (
typeof propertyArgument === "undefined" ||
typeof propertyArgument === "string" ||
Array.isArray(propertyArgument)
) {
// access private fields of OfficeMockObject ...
const mockItemsObj = { ...this["items"] }
const properties = mockItemsObj._properties
if (!properties) {
// does not have an items property
return this._load(propertyArgument)
} else {
const mockLengthObj = { ...properties.get("length") }
const length = mockLengthObj._valueBeforeLoaded ? (mockLengthObj._valueBeforeLoaded as number) : 0
if (typeof propertyArgument === "undefined") {
return this._load("*")
} else {
const names = Array.isArray(propertyArgument)
? (propertyArgument as string[]).filter((name) => name.trim() !== "items")
: propertyArgument?.split(",").filter((name) => name.trim() !== "items")
const indices = [...Array(length).keys()]
const paths = ["items"].concat(
indices.flatMap((index) => names.map((name) => `items/${index}/${name.trim()}`)),
)
return this._load(paths)
}
}
} else {
return this._load(propertyArgument)
}
}
}
With the patch in place, the example code will now work as expected, with the fields.items.length
, fields.items[i].code
and fields.items[i].result
returning the expected values.
Prerequisites
Please answer the following questions before submitting an issue. YOU MAY DELETE THE PREREQUISITES SECTION.
Expected behavior
Please describe the behavior you were expecting
I am trying to mock Named ranges array of excel so i can run some tests on adding and removing custom ranges, but it appears that the mock object is not equipped to handle arrays?
Using Jest and in Angular.
As per the documentation i am mocking the Host object and assigning it to the global.Excel
Then in my service
The line mentioned above produces the following error
Changing the code slightly to instead use context.load(workbook.names, 'items') produces a different error
Also trying workbook.names.load('items') works but items is undefined even though it was clearly defined in the mock object.
All of the ways tested above work when the code is run properly within an office environment and only fail with mocking.
The only workaround that worked was:
Expected behaviour
I would expect any code that runs fine when not under test to work with the mock object as well.