I can do this a couple of ways, but let's stick to the AddIn itself. If I create a ContentControl:
Word.run((context) => {
let control = context.document.getSelection().insertContentControl();
control.tag = 'example';
control.insertOoxml('<xml here>');
context.sync();
});
Then later (with proper async handling of course) I delete the control:
Word.run((context) => {
let contentControls = context.document.contentControls;
context.load(contentControls, 'tag');
context.sync().then(() => {
for (let c = 0; c < contentControls.items.length; ++c) {
if (contentControls.items[c].tag === 'example') {
contentControls.items[c].delete(false); // delete the contentControl with the matching tag
}
}
context.sync()
});
});
Then I check the list of content controls for that control I just deleted:
Word.run((context) => {
let contentControls = context.document.contentControls;
context.load(contentControls, 'tag');
context.sync().then(() => {
for (var i = 0; i < contentControls.items.length; ++i) {
if (contentControls.items[c].tag === 'example') {
// this tag list still includes the deleted content control until I close and reopen the document
}
}
});
});
The tags still show the deleted content control. I have to close and reopen the document for the context to refresh. Am I missing a step for proper syncing with the current state of the document? Is context.sync() not enough?
NOTE: delete() does work: the content disappears from the document as expected. It's just still in the list of controls when I search the document.
Further research has determined the cause of this.
When TrackChanges is on, the ContentControls in the document are not ACTUALLY deleted. This still feels like a word bug, but you can check for the deleted flag on the contentcontrol (not sure offhand if that's really possible because it might be at some arbitrary level of the ancestry), manage your deletions manually (as we're doing), or turn off track changes to resolve this issue.
All that said, I am going to leave this up as a question in case someone has a better resultion.
Try to check list after you sync your context - in .then callback:
Word.run((context) => {
let contentControls = context.document.contentControls;
context.load(contentControls, 'tag');
context.sync().then(() => {
for (let c = 0; c < contentControls.items.length; ++c) {
if (contentControls.items[c].tag === 'example') {
contentControls.items[c].delete(false); // delete the contentControl with the matching tag
}
}
context.sync().then(()=> {
//your updated list
})
});
});
Related
I want to be able to change the value of a global variable when it is being used by a function as a parameter.
My javascript:
function playAudio(audioFile, canPlay) {
if (canPlay < 2 && audioFile.paused) {
canPlay = canPlay + 1;
audioFile.play();
} else {
if (canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
};
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
btnPitch01.addEventListener("click", function() {
playAudio(audioFilePitch01, canPlayPitch01);
});
My HTML:
<body>
<button id="btnPitch01">Play Pitch01</button>
<button id="btnPitch02">Play Pitch02</button>
<script src="js/js-master.js"></script>
</body>
My scenario:
I'm building a Musical Aptitude Test for personal use that won't be hosted online. There are going to be hundreds of buttons each corresponding to their own audio files. Each audio file may only be played twice and no more than that. Buttons may not be pressed while their corresponding audio files are already playing.
All of that was working completely fine, until I optimised the function to use parameters. I know this would be good to avoid copy-pasting the same function hundreds of times, but it has broken the solution I used to prevent the audio from being played more than once. The "canPlayPitch01" variable, when it is being used as a parameter, no longer gets incremented, and therefore makes the [if (canPlay < 2)] useless.
How would I go about solving this? Even if it is bad coding practise, I would prefer to keep using the method I'm currently using, because I think it is a very logical one.
I'm a beginner and know very little, so please forgive any mistakes or poor coding practises. I welcome corrections and tips.
Thank you very much!
It's not possible, since variables are passed by value, not by reference. You should return the new value, and the caller should assign it to the variable.
function playAudio(audioFile, canPlay) {
if (canPlay < 2 && audioFile.paused) {
canPlay = canPlay + 1;
audioFile.play();
} else {
if (canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
return canPlay;
};
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
btnPitch01.addEventListener("click", function() {
canPlayPitch01 = playAudio(audioFilePitch01, canPlayPitch01);
});
A little improvement of the data will fix the stated problem and probably have quite a few side benefits elsewhere in the code.
Your data looks like this:
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
// and, judging by the naming used, there's probably more like this:
const btnPitch02 = document.getElementById("btnPitch02");
const audioFilePitch02 = new Audio("../aud/Pitch02.wav");
var canPlayPitch02 = 0;
// and so on
Now consider that global data looking like this:
const model = {
btnPitch01: {
canPlay: 0,
el: document.getElementById("btnPitch01"),
audioFile: new Audio("../aud/Pitch01.wav")
},
btnPitch02: { /* and so on */ }
}
Your event listener(s) can say:
btnPitch01.addEventListener("click", function(event) {
// notice how (if this is all that's done here) we can shrink this even further later
playAudio(event);
});
And your playAudio function can have a side-effect on the data:
function playAudio(event) {
// here's how we get from the button to the model item
const item = model[event.target.id];
if (item.canPlay < 2 && item.audioFile.paused) {
item.canPlay++;
item.audioFile.play();
} else {
if (item.canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
};
Side note: the model can probably be built in code...
// you can automate this even more using String padStart() on 1,2,3...
const baseIds = [ '01', '02', ... ];
const model = Object.fromEntries(
baseIds.map(baseId => {
const id = `btnPitch${baseId}`;
const value = {
canPlay: 0,
el: document.getElementById(id),
audioFile: new Audio(`../aud/Pitch${baseId}.wav`)
}
return [id, value];
})
);
// you can build the event listeners in a loop, too
// (or in the loop above)
Object.values(model).forEach(value => {
value.el.addEventListener("click", playAudio)
})
below is an example of the function.
btnPitch01.addEventListener("click", function() {
if ( this.dataset.numberOfPlays >= this.dataset.allowedNumberOfPlays ) return;
playAudio(audioFilePitch01, canPlayPitch01);
this.dataset.numberOfPlays++;
});
you would want to select all of your buttons and assign this to them after your html is loaded.
https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName
const listOfButtons = document.getElementsByClassName('pitchButton');
listOfButtons.forEach( item => {
item.addEventListener("click", () => {
if ( this.dataset.numberOfPlays >= this.dataset.allowedNumberOfPlays ) return;
playAudio("audioFilePitch" + this.id);
this.dataset.numberOfPlays++;
});
Relatively new to writing end to end tests with Protractor. Also relatively inexperienced at working with promises.
I am writing a test where in some cases I need to loop through my code b/c the record that I select does not meet certain criteria. In those cases I would like to proceed back to a previous step and try another record (and continue doing so until I find a suitable record). I am not able to get my test to enter into my loop though.
I can write regular e2e tests with Protractor, but solving this looping issue is proving difficult. I know it must be because I'm dealing with Promises, and am not handling them correctly. Although I've seen examples of looping through protractor code, they often involve a single method that needs to be done to every item in a list. Here I have multiple steps that need to be done in order to arrive at the point where I can find and set my value to break out of the loop.
Here are some of the threads I've looked at trying to resolve this:
protractor and for loops
https://www.angularjsrecipes.com/recipes/27910331/using-protractor-with-loops
Using protractor with loops
Looping through fields in an Angular form and testing input validations using Protractor?
Protractors, promises, parameters, and closures
Asynchronously working of for loop in protractor
My code as it currently stands:
it('should select a customer who has a valid serial number', () => {
const products = new HomePage();
let serialIsValid: boolean = false;
let selectedElement, serialNumber, product, recordCount, recordList;
recordList = element.all(by.css(`mat-list.desco-list`));
recordList.then((records) => {
recordCount = records.length;
console.log('records', records.length, 'recordCount', recordCount);
}
);
for (let i = 0; i < recordCount; i++) {
if (serialIsValid === false) {
const j = i + 1;
products.btnFormsSelector.click();
products.formSelectorRepossession.click();
browser.wait(EC.visibilityOf(products.itemSearch));
products.itemSearch.element(by.tagName('input')).sendKeys(browser.params.search_string);
products.itemSearch.element(by.id('btnSearch')).click();
browser.wait(EC.visibilityOf(products.itemSearch.element(by.id('list-container'))));
selectedElement = element(by.tagName(`#itemSearch mat-list:nth-child(${{j}})`));
selectedElement.click();
browser.wait(EC.visibilityOf(products.doStuffForm));
browser.sleep(1000);
element(by.css('#successful mat-radio-button:nth-child(1) label')).click();
browser.sleep(1000);
expect(element(by.css('.itemDetailsContainer'))).toBeTruthy();
product = products.productIDNumber.getText();
product.then((item) => {
serialNumber = item;
if (item !== 'Unknown') {
expect(serialNumber).not.toContain('Unknown');
serialIsValid = true;
} else {
i++
}
})
} else {
console.log('serial is valid: ' + serialIsValid);
expect(serialNumber).not.toContain('Unknown');
break;
}
}
console.log('serial number validity: ', serialIsValid);
})
I have rewritten and reorganized my code several times, including trying to break out my code into functions grouping related steps together (as recommended in one of the threads above, and then trying to chain them together them together, like this:
findValidCustomer() {
const gotoProductSearch = (function () {...})
const searchForRecord = (function () {...})
const populateForm = (function (j) {...})
for (let i = 0; i < recordCount; i++) {
const j = i + 1;
if (serialIsValid === false) {
gotoProductSearch
.then(searchForRecord)
.then(populateForm(j))
.then(findValidSerial(i))
} else {
console.log('serial number validity' + serialIsValid);
expect(serialIsValid).not.toContain('Unknown');
break;
}
}
console.log('serial number validity' + serialIsValid);
}
When I've tried to chain them like that, I received this error
- TS2345: Argument of type 'number | undefined' is not assignable to parameter of type 'number'
Have edited my code from my actual test and apologies if I've made mistakes in doing so. Would greatly appreciate comments or explanation on how to do this in general though, b/c I know I'm not doing it correctly. Thanks in advance.
I would suggest looking into async / await and migrating this test. Why migrate? Protractor 6 and moving forward will require async / await. In order to do that, you will need to have SELENIUM_PROMISE_MANAGER: false in your config and await your promises. In my answer below, I'll use async / await.
Below is my attempt to rewrite this as async / await. Also try to define your ElementFinders, numbers, and other stuff when you need them so you can define them as consts.
it('should select a customer who has a valid serial number', async () => {
const products = new HomePage();
let serialIsValid = false; // Setting the value to false is enough
// and :boolean is not needed
const recordList = element.all(by.css(`mat-list.desco-list`));
const recordCount = await recordList.count();
console.log(`recordCount ${recordCount}`);
// This could be rewritten with .each
// See https://github.com/angular/protractor/blob/master/lib/element.ts#L575
// await recordList.each(async (el: WebElement, index: number) => {
for (let i = 0; i < recordCount; i++) {
if (serialIsValid === false) {
const j = index + 1; // Not sure what j is being used for...
await products.btnFormsSelector.click();
await products.formSelectorRepossession.click();
await browser.wait(EC.visibilityOf(products.itemSearch));
await products.itemSearch.element(by.tagName('input'))
.sendKeys(browser.params.search_string);
await products.itemSearch.element(by.id('btnSearch')).click();
await browser.wait(
EC.visibilityOf(await products.itemSearch.element(
by.id('list-container')))); // Maybe use a boolean check?
const selectedElement = element(by.tagName(
`#itemSearch mat-list:nth-child(${{j}})`));
await selectedElement.click();
// not sure what doStuffForm is but I'm guessing it returns a promise.
await browser.wait(EC.visibilityOf(await products.doStuffForm));
await browser.sleep(1000); // I would avoid sleeps since this might
// cause errors (if ran on a slower machine)
// or just cause your test to run slow
await element(by.css(
'#successful mat-radio-button:nth-child(1) label')).click();
await browser.sleep(1000);
expect(await element(by.css('.itemDetailsContainer'))).toBeTruthy();
const serialNumber = await products.productIDNumber.getText();
if (item !== 'Unknown') {
expect(serialNumber).not.toContain('Unknown');
serialIsValid = true;
}
// The else statement if you were using i in a for loop, it is not
// a good idea to increment it twice.
} else {
// So according to this, if the last item is invalid, you will not break
// and not log this. This will not fail the test. It might be a good idea
// to not have this in an else statement.
console.log(`serial is valid: ${serialIsValid}`);
expect(serialNumber).not.toContain('Unknown');
break;
}
}
console.log('serial number validity: ', serialIsValid);
});
Can you check the count again after updating your code by following snippet
element.all(by.css(`mat-list.desco-list`)).then(function(records) => {
recordCount = records.length;
console.log(recordCount);
});
OR
There is count() function in ElementArrayFinder class which returns promise with count of locator
element.all(by.css(`mat-list.desco-list`)).then(function(records) => {
records.count().then(number => {
console.log(number); })
});
I am building a simple application that stores places I've visited. I have a local express server using a db.json file as my database. I am making 2 requests and experiencing a problem.
What I'm trying to do is iterate over both arrays so that when the app loads, countries I've been too are already preselected. this seems like a super expensive call to make and has quite slow performance already
Also it's not actually doing what I want until I trigger a second re-render of the DOM and then it updates.
e.g. if I pre-select Croatia and France in the database and then load the app, none are selected. but if I then select Korea (e.g.) then in the visited list, suddenly all 3 are visible
what would be a better way to compare the arrays? considering the object keys are not necessarily the same
componentDidMount(){
axios.get('https://restcountries.eu/rest/v2/all').then((data) => {
const updatedCountries = data.data.map((country) => {
return {...country, visited: false, cities: [], showCities: false}
})
axios.get('http://localhost:3007/countries').then((countries) => {
const visitedCountries = countries.data
for (var i = 0; i < visitedCountries.length; i++){
for (var k = 0; k < updatedCountries.length; k++){
if(visitedCountries[i].name === updatedCountries[k].name){
updatedCountries[k].visited = true
}
}
}
})
this.setState({countries: updatedCountries})
})
}
Instead of using an array to store updatedCountries, you should instead use an object. That way instead of having each element of updatedCountries compare to every element of visitedCountries, you can do a constant lookup. This will change your lookup speed from (n*n) to (n).
The reason why you do not initially see any updates is because you have an async call:
axios.get('http://localhost:3007/countries')
inside of a synchronous function. As a result, you are resetting the state while you are making the get request. Instead you should chain your api calls like
axios.get('https://restcountries.eu/rest/v2/all').then((data) => {
// edit data
return axios.get('http://localhost:3007/countries')
}).then((data) => {
// run function comparing both data
this.setState({countries: updatedCountries})
})
You need update state in second request success callback function
componentDidMount(){
axios.get('https://restcountries.eu/rest/v2/all').then((data) => {
const updatedCountries = data.data.map((country) => {
return {...country, visited: false, cities: [], showCities: false}
})
axios.get('http://localhost:3007/countries').then((countries) => {
const visitedCountries = countries.data
for (var i = 0; i < visitedCountries.length; i++){
for (var k = 0; k < updatedCountries.length; k++){
if(visitedCountries[i].name === updatedCountries[k].name){
updatedCountries[k].visited = true
}
}
}
this.setState({countries: updatedCountries})
})
})
}
For efficient way to search
axios.get('http://localhost:3007/countries').then((countries) => {
let visitedCountriesName = new Set(countries.data.map(country => country.name));
updatedCountries = updatedCountries.map((country) => {
if (visitedCountriesName.has(country.name)) country.visited = true
return country
});
this.setState({countries: updatedCountries})
})
I wrote some code that checks a list, and checks if each item in the list is present in the other one. If the item isn't found, it adds it to the database.
The scanning code is correct (the part that says db.scan) but somewhere towards the end the code isn't going through because its not executing the console.log part (Where it says "Entering journal into database..." title of article"
When I execute this code, nothing happens. At least there are no errors... but its not even logging the console.log parts so something is wrong.
// accessing the database
function DatabaseTime(sourcesDates, timeAdded, links, titles, descriptions) {
sourcesDates = sourcesDates;
links = links;
titles = titles; // this will be used to check on our articles
descriptions = descriptions;
var autoParams;
var databaseOperation = function (sourcesDates, timeAdded, links, titles, descriptions) {
var scanParams = { TableName: "Rnews" }
// using code to setup for accessing the 2nd list
db.scan(scanParams, function(err, scanData) { // scanData = the 2nd list we are going to work with
var counter = 0; // just a way to help make my code more accurate as seen later in the loops
var counter2 = 0;
// this is the first list iterating on
for (var i = 0; i < links.length; i++) {
counter = 0;
// looping through items in second list
for (var x = 0; x < scanData.Items.length; x++) {
// if article is not in db
if (titles[i] !== scanData.Items[x].title) {
continue;
}
else if (titles[i] === scanData.Items[x].title) {
// intention is to immediately move to the next item in the first list if this block executes
console.log("Article found: \"" + titles[i] + "\". Not proceeding anymore with article.");
counter++;
break;
} else {
// if this article isnt found anywhere in the list we are checking on, add to database
if (x === scanData.Items.length && counter !== 0) {
autoParams = {
TableName: "Rnews",
Item: {
title: titles[i],
source: sourcesDates[i],
url: links[i],
description: descriptions[i],
lastAddOrUpdated: dbTimeStamp,
timePublish: timeAdded[i]
}
}
console.log("Entering journal to database: " + titles[i]);
db.put(autoParams, function(err, data) {
if(err) throw err;
});
//}
}
}
}
}
});
//console.log("Complete");
};
databaseOperation(sourcesDates, timeAdded, links, titles, descriptions);
}
//// END
You never called the function DatabaseTime. Your code just declares the function and does nothing else. In order for the function to execute, you must invoke it.
Code below is in C#, but I also know javascript/protractor. Looking for any pattern that works.
var links = driver.FindElements(By.TagName("a"));
foreach (var ele in links)
{
if (ele.Displayed == false) continue;
if (ele.Enabled) ele.Click();
System.Threading.Thread.Sleep(3000);
driver.Navigate().Back();
System.Threading.Thread.Sleep(3000);
}
Without the sleep above (which I don't like) the page hasn't settled down enough to Navigate Back. With the sleep values in, I can click the link, and go back but only one time! The error on 2nd iteration tells me that the page is stale.
Question: Using Selenium with C# or Protractor how do I go through entire list of links?
If these links are regular links with href attributes, you can use map() to get the array of hrefs, and navigate to each of them one by one. protractor-specific solution:
element.all(by.tagName("a")).map(function (a) {
return a.getAttribute("href");
}).then(function (links) {
for (i = 0; i < links.length; i++) {
browser.get(links[i]);
// TODO: some logic here
}
});
This solution below works for C# without the MAP option pointed out above. Design was to first find the links and put each element's location and text values into a list named "Locators". Then for each tuple in that "Locators" list, pull a fresh copy each time before the click and page back methods.
var links = driver.FindElements(By.TagName("a"));
var Locators = new List<Tuple<Point, string>>();
foreach (var thing in links)
{
var tup = new Tuple<Point, string>(thing.Location, thing.Text);
Locators.Add(tup);
}
foreach (var thing in Locators)
{
var pt = thing.Item1;
var reassess = driver.FindElements(By.TagName("a"));
var filtered = reassess.ToList<IWebElement>().Where(
p =>
p.Location == thing.Item1 &&
p.Text == thing.Item2 &&
p.Displayed == true
);
// Debugger.Break();
if (filtered.Count() == 0) continue;
filtered.First().Click();
driver.WaitForPageToLoad();
AssessNewPageContent();
driver.Navigate().Back();
driver.WaitForPageToLoad();
}
The AssessNewPageContent does assertions and could be a callback if you prefer that.
The code for WaitForPageToLoad was lifted from the internet somewhere and looks like this:
public static IWebDriver WaitForPageToLoad(this IWebDriver driver)
{
TimeSpan timeout = new TimeSpan(0, 0, 30);
WebDriverWait wait = new WebDriverWait(driver, timeout);
IJavaScriptExecutor javascript = driver as IJavaScriptExecutor;
if (javascript == null)
throw new ArgumentException("driver", "Driver must support javascript execution");
wait.Until((d) =>
{
try
{
string readyState = javascript.ExecuteScript("if (document.readyState) return document.readyState;").ToString();
return readyState.ToLower() == "complete";
}
catch (InvalidOperationException e)
{
//Window is no longer available
return e.Message.ToLower().Contains("unable to get browser");
}
catch (WebDriverException e)
{
//Browser is no longer available
return e.Message.ToLower().Contains("unable to connect");
}
catch (Exception)
{
return false;
}
});
return driver;
}