How to loop through a RangeCollection in Word JS API? - ms-word

I want to add a Find function in my Word Add-in. By clicking a button, the Word will scroll to the first place of the word need to be searched, and by clicking again, the Word will scroll to the next place. I figured out how to find the first one, but I can't loop through the RangeCollection of the search results of the word. I am looking for some functions similar to getNext()
Below is the code to find first "and". It works:
async function scroll() {
await Word.run(async (context) => {
context.document.body.search("and", { matchWholeWord: true, matchCase: false }).getFirst().select();
await context.sync()
}
)}
However, if I want to find the second or more, I can't do this:
async function scroll() {
await Word.run(async (context) => {
context.document.body.search("and", { matchWholeWord: true, matchCase: false }).getFirst().getNext().select();
await context.sync()
} )}
I read the document and there is no getNext() for RangeCollection. Hope somebody knows how to work around it. Thank you!

There are a few workarounds.
Solution 1
Load RangeCollection.items and record the index. Example:
var index = 0;
async function selectNext(...) {
...
var ranges = context.document.body.search(...);
ranges.load('items');
await context.sync();
if (index >= ranges.items.length) {
index = 0;
}
ranges.items[index++].select();
await context.sync();
...
}
This solution respects the index, but not the current selection position. So if you prefer to search downward instead of searching the next, here's solution 2.
Solution 2
Load RangeCollection.items and call Range.compareLocationWith. Example:
var ranges = context.document.body.search(...);
ranges.load('items');
await context.sync();
var selection = context.document.getSelection();
var compareResults = ranges.items.map(range => range.compareLocationWith(selection));
await context.sync();
for(var index in compareResults) {
if (compareResults[index].value == 'After') {
ranges.items[index].select();
await context.sync();
break;
}
}
Of course you would need additional code and polish it to make it a real product, but these are the rough ideas. Please see if they work for you.

Related

Office-js Word addin, Grabbing document Text

In my code below, I am attempting to grab the current documents text, set it to a variable and then use that variable in a call. My promise may not be formatted correctly but essentially the getScanResult() function uses the docBodyText variable that I set in handleClickRun(). Everytime I call it the variable is empty. Any idea as to why the document text is not being captured correctly?
const [docBodyText, setDocBodyText] = useState('');
const handleClickRun = async () => {
return Word.run(async (context: Word.RequestContext) => {
const docBody = context.document.body;
docBody.load("text");
await context.sync();
setDocBodyText(docBody.text);
await context.sync();
})
.catch(function (error) {
console.log("Error: " + error);
if (error instanceof OfficeExtension.Error) {
console.log("Debug info: " + JSON.stringify(error.debugInfo));
}
});
};
const handleScanResults = () => {
new Promise(async (resolve) => {
await handleClickRun();
await getScanResult();
resolve('Completed')
})};
I have tried using the docs and looking for other examples but have not seen any other use cases. The docs I am using is this Perhaps I can be pointed to the correct method.
I have also tried making a variable of just plain text and passing it to my api call and it works perfectly fine, so it is not a call issue.

Attaching paragraph to the top of nested list highlights level change

I'm working on a simple office-js Word app, built with the Yeoman Generator. I have a very basic document with the following structure:
1. This is heading one
1.1 This is subheading 1
1.2 This is subheading 2
Some random text here...
Paragraph I want to attach to the list from the beginning of my doc
When I try to attach this last paragraph to my list though, I'm getting the following output:
Word document's screenshot:
It looks as though it's first trying to insert it to the level=1 of that list (thus the initial 3) and then actually inserts it to the root list's position (thus the secondary 2).
The code I'm running looks as follows:
const insertHeading = async (event: Office.AddinCommands.Event) => {
try {
await Word.run(async (context) => {
const selection = context.document.getSelection();
selection.load("paragraphs");
await context.sync();
const lists = context.document.body.lists;
const list = lists.getFirst();
const paragraph = selection.paragraphs.getFirst();
list.load("id");
await context.sync();
paragraph.attachToList(list.id, 0);
await context.sync();
})
} catch (error) {
console.info("Error occurred", error.debugInfo);
} finally {
event.completed();
}
}
I'd expect this paragraph to be added as 2nd item of the root level (with 2.) and without this turquoise strike, shown in the image above.
Any help would be much appreciated!

Flutter for loop containing .then statement completing first half then second half

I have a list called wastedProducts which I want to iterate through and create a new list, wastedProductsSet based on each item in the original list.
This is the code I have to do that:
for (var productHeld in wastedProducts) {
if (wastedProductsSetNames.contains(productHeld.masterCat)) {
print("duplicate ${productHeld.masterCat}");
} else {
_wastedCount = wastedProducts
.where((p) => p.masterCat == productHeld.masterCat)
.fold(0,(amountWasted, product) => amountWasted + product.amountWasted);
_masterCat = Firestore.instance
.collection('masterCategories')
.document(productHeld.masterCat);
// repeats the above for each item before completing the below for each item
_masterCat.get().then(
(value) {
baseUnit = value.data['baseUnit'];
if (baseUnit == null) {
baseUnit = '';
} else {
baseUnit = value.data['baseUnit'];
}
wastedProductsSet.add(
WastedProduct(
productName: productHeld.masterCat,
wastedCount: _wastedCount.toInt(),
baseUnit: baseUnit,
),
);
wastedProductsSetNames.add(productHeld.masterCat);
},
);
}
}
Based on the print statements, I can see it is completing the code up to the _masterCat.get().then( line and doing that for each item in wastedProducts, then completing the code below _masterCat.get().then( for each item.
I assume it must have something to do with the asynchronous nature of the .then but cannot work out what the problem is.
I originally was using .forEach instead of for (var productHeld in wastedProducts) but changed based on the answer in this post My async call is returning before list is populated in forEach loop.

Protractor: How do I create a function that receives an HTML element as a parameter?

I'm new to Protractor.
I'm trying to select a button based on the button title. I want to make this into a function, and pass the button title in as a parameter.
This is the hard-coded version which works:
it('I click on a button based on the button title', async function() {
let button = element(by.css('button[title=example_button_title]'));
await button.click();
});
I created a global variable and a function to try and replace this, where 'buttonTitle' is the parameter I'm passing into the function:
Variable:
let dynamicButton = buttonTitle => { return element(by.css("'button[title=" + buttonTitle + "]'")) };
Function:
this.selectDynamicButton = async function(buttonTitle) {
await browser.waitForAngularEnabled(false);
await dynamicButton(buttonTitle).click();
};
When I try this I get the following error:
Failed: invalid selector: An invalid or illegal selector was specified
Apologies if there appear to be basic errors here, I am still learning. I appreciate any help that anyone can give me. Thanks.
You can add a custom locator using protractors addLocator functionality. (this is actually a very similar use case to the example listed in the link)
This would look like the following:
onPrepare: function () {
by.addLocator('buttonTitle', function (titleText, opt_parentElement) {
// This function will be serialized as a string and will execute in the
// browser. The first argument is the text for the button. The second
// argument is the parent element, if any.
const using = opt_parentElement || document;
const matchingButtons = using.querySelectorAll(`button[title="${titleText}"]`);
let result = undefined;
if (matchingButtons.length === 0) {
result = null;
} else if (matchingButtons.length === 1) {
result = matchingButtons[0];
} else {
result = matchingButtons;
}
return result;
});
}
This is called like
const firstMatchingButton = element(by.buttonTitle('example_button_title'));
const allMatchingButtons = element.all(by.buttonTitle('example_button_title'));
I had to edit this code before posting so let me know if this does not work. My work here is largely based off this previous answer
let dynamicButton = buttonTitle => { return element(by.css('button[title=${buttonTitle} ]')) };
Use template literals instead of string concatenation with +.
Protractor already has a built in locator which allows you to get a button using the text. I think you are looking at something like that. See the element(by.buttonText('text of button')) locator.
For more reference see here.

Protractor tests are inconsistent even with browser.wait and helper functions

I am looking for some advice. I have been using protractor for a few weeks and just cannot get my tests to be consistent unless I use browser.sleep. I have tried helper functions as well as browser.wait(expectedCondition). I have reduced browser.sleep immensely, but protractor still just goes way to fast. I can never successfully run multiple tests unless I have a few browser.sleeps just so protractor can relax for a second. Here is an example:
The Test I need to select a user, delete that user and wait for a success message. Then I click the same user and click the activate button.
Outcome: Unless I have browser.sleep, my success messages do not even appear after deletion/activation. The tests fail because protractor is moving way too fast. Even with the expected conditions. My main problem is that protractor moves to fast for the angular web page. I have tried ifCLickable or isDisplayed but they do not fix the issue entirely. Here is the code:
async deleteUser() {
await sendClick(this.getNewUser());
await sendClick(this.getDelete());
await waitTillPresent(this.getDeleteConfirm());
await sendClick(this.getDeleteConfirm());
await waitTillPresent(this.getSuccessMsg())
expect(await page.getSuccessMsg().isDisplayed()).toBeTruthy();
}
async activateUser() {
await sendClick(this.getNewUser());
await waitTillPresent(this.getEditBtn())
await sendClick(this.getActive());
await waitTillPresent(this.getSuccessMsg())
expect(await page.getSuccessMsg().isDisplayed()).toBeTruthy();
}
Functions:
export async function sendClick(element: ElementFinder): Promise<boolean> {
try {
if(!await element.isDisplayed()) {
return false;
}
await browser.executeScript('arguments[0].click();', await element.getWebElement());
return true;
}
catch (err) {
return false;
}
}`
export async function waitTillPresent (element: ElementFinder, timeout: number = 10000) {
return browser.wait(() => {
return element.isPresent();
}, timeout);
}
My Question: Am I handling this correctly? Is there a better to ensure my tests are consistent? Before these tests, I visit a non-angular webpage. So I have to include the line browser.waitForAngularEnabled(false)
Does this mess with the async nature of angular? Thank you.
I worked the last few months on our e2e test suite to make it stable. I did not believe it's possible but I made it using correct wait functions and sometimes browser.sleep() as a last resort.
You have a correct approach for waiting for elements. But there are 2 problems regarding your implementation:
1) The function waitTillPresent() does exactly what its name stands for. But if you only wait until the element is present on the page it does not mean it's clickable or displayed. An element can be hidden and at the same time still be present. Please rename waitTillPresent() to waitTillDisplayed() and change it as follows:
export async function waitTillDisplayed(element: ElementFinder, timeout: number = 20000): Promise<boolean> {
let result = await browser.wait(() => element.isPresent(), timeout);
if(!result) {
return false;
}
return await browser.wait(element.isDisplayed(), timeout);
}
2) You should exceed the default timeout. Set it a bit higher like 20 to 25 seconds. Just play with it.
Unfortunately, I don't know how browser.waitForAngularEnabled(false) changes test behavior. We do not use it :)
Note:
These functions are all exactly the same:
function foo() { return 'hello world'; }
var foo = () => { return 'hello world'; };
var foo = () => 'hello world';
Play with arrow functions, it's syntactic sugar.
Cheers and gl!