Get XML node children using a variable - powershell

Given the following file:
<environments>
<dev>
<property1>23</property1>
<property2>blue</property2>
<property3>apple</property3>
</dev>
<prod>
<property1>27</property1>
<property2>red</property2>
<property3>orange</property3>
</prod>
</environments>
And the following code:
$environmentsFile = [System.Xml.XmlDocument](Get-Content "environments.xml");
$envs = $environmentsFile.environments;
$envName = "prod"
How would I access the "prod" children and iterate through them? I'm already iterating through the environments using ForEach ($env in $envs.ChildNodes) {} and I know I oculd do $envs.prod
I tried this:
$test = $envs.SelectNodes("//$envName")
ForEach ($ele in $test) {
Write-Host $ele.value
}
But it didn't output anything. Inspecting reveals that it looks like $test has a single node that contains all the nodes I want, so I tried $test[0] and that didn't work either.

I had done a couple things wrong. This functions as desired:
$test = $envs.SelectNodes("//$envName")[0]
ForEach ($ele in $test.ChildNodes) {
Write-Host "$($ele.Name): $($ele.InnerText)"
}

Related

Pester: Use variables in -ForEach data driven tests with rich content

I write a module used for powershell-wrapping a commandline tool.
For my tests I had some variables defined in BeforeAll. Those vars should help in feeding the command with desired params and check them against the expected result, f.i.
BeforeAll {
$PORead=#{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
$POReadExp="-bla bla (bla:bla;fra:fra;tra:tra)"
}
Now I want to use these in an it segment to feed the function and check the result, like:
It "checks result" -ForEach (
#{Param=$PORead;Expected=$POReadExp}
) {
$result=FunctionCall -Parameter1 $Param
$result | Should -Be $Expected
}
Note: This is actually no working code - just to make clear what I want.
The problem is, that $Param and $Expected are $null, thus the current value of $PORead and $POReadExp are not assigned to $Param and $Expected.
I made it easy to demonstrate the problem - in my code I use arrays in the -ForEach hash assigned to $Param and $Expected.
Using plain strings, there is no problem and everything works fine.
I could use plain text and than use som switch construct to match my strings with the according variables from BeforeAll{}, but I thought there must be a more elegant way.
Using standard hashtables, the variable substition works fine.
I'd appreciate any idea.
To clarify some things:
The solution of Mark works somehow, but it needs the $PORead and $POReadExp to be set outside the BeforeAll-block (if you want to use -ForEach), or within the BeforeAll-block, but in that case you only can use one argument.
My plan was a bit different, so I'm making my point a bit clearer:
In the BeforeAll-block I wanted a bunch of definitions for params and outcomes
BeforeAll {
$PORead = #{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
$PODelete = #{
Name = "Paul"
Permission ="Delete"
Inheritance = "ThisFolderOnly"
}
$POReadResult = "add parameters"
$PODeleteResult = "delete parameters"
}
Now I want to test a whole bunch of combinations, feeding different combinations to the function and checking, if the combination of results match:
It "Test if all combinations works" -ForEach(
#{FunctionParam=$PORead;Expected=$POReadExp}
#{FunctionParam=$PORead,$PORead;Expected=$POReadExp,$POReadExp}
#{FunctionParam=$PORead,$PODelete,$PORead,$PORead;Expected=$PORead,$PORead,$PORead,$PODelete}
) {
$result = Function-Call -Param $FunctionParam
$wanted = $expected -join(" ")
$result | Should -Be $wanted
}
I can realize this by placing the definitions out of any block, but it feels wrong in terms of pester coding guidelines.
Greets
Usul
The scopes for how to set test data with Pester v5 are quite confusing. What I've found is that anything you set in a BeforeAll or BeforeEach is available within the It. However if you want to set values to the -ForEach (or its alias -TestData) you need to do it as part of or before the It, outside of the BeforeEach or BeforeAll.
As such I think your tests will work if you do this:
$PORead = #{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
$POReadExp = "-bla bla (bla:bla;fra:fra;tra:tra)"
It "checks result" -ForEach (
#{Param=$PORead;Expected=$POReadExp}
) {
$result=FunctionCall -Parameter1 $Param
$result | Should -Be $Expected
}
Or alternatively (if you just need to perform a single test):
BeforeAll {
$PORead=#{
Name = "Paul"
Permission ="Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
$POReadExp="-bla bla (bla:bla;fra:fra;tra:tra)"
}
It "checks result" {
$result = FunctionCall -Parameter1 $PORead
$result | Should -Be $POReadExp
}
If you want to avoid using a variable that is not within one of the Pester code blocks, then you should do this (which is in line with the examples given in the Pester documentation on Data Driven Tests):
It "checks result" -ForEach (
#{
Param = #{
Name = "Paul"
Permission = "Read"
Inheritance = "ThisFolderSubfoldersAndFiles"
}
Expected = "-bla bla (bla:bla;fra:fra;tra:tra)"
}) {
$result=FunctionCall -Parameter1 $Param
$result | Should -Be $Expected
}

Is it possible to create specific constant elements of a PowerShell array?

Let's say I have array Grid that was initialized with
$Grid = #(#(1..3), #(4..6), #(7..9))
and I want to change Grid[0][0] to value "Test" but I want to make it unchangeable, is there a way I could to that?
So far I've tried playing around with classes that allow read-only or constant declarations through the class as opposed to using New-Variable/Set-Variable but it doesn't affect the index itself but the individual element as in
$Grid[0][0] = [Array]::AsReadOnly(#(1,2,3))
$Grid[0][0] # 1 \n 2 \n 3
$Grid[0][0].IsReadOnly # True
$Grid[0][0] = "test"
$Grid[0][0] # test
I assume this is due to $Grid[0][0] being read-only as opposed to constant and the behaviour I experienced supported that:
$test = [Array]::AsReadOnly(#(1,2,3,4))
$test[0]=1 # Errors
$test = "test"
$test # test
$Grid = #(#(1..3), #(4..6), #(7..9))
$Grid[0][0] = [Array]::AsReadOnly(#(1,2,3))
$Grid[0][0][0] = 1 # Errors
$Grid[0][0] = "test"
$Grid[0][0] # test
I'm not sure what to try next and I know that this is very simple with classes but I am not looking for that as a solution.
You'll have to make both dimensions of your nested array read-only to prevent anyone from overwriting $grid[0]:
$grid =
[array]::AsReadOnly(#(
,[array]::AsReadOnly(#(1,2,3))
,[array]::AsReadOnly(#(3,2,1))
))
(the unary , is not a typo, it prevents PowerShell from "flattening" the resulting read-only collection)
Now $grid should behave as you expect:
$grid[0] # 1,2,3
$grid[0][0] # 1
$grid[0][0] = 4 # error
$grid[0] = 4 # error
If you want to be able to prevent writing to individual "cells", you'll have to define a custom type:
using namespace System.Collections
class Grid {
hidden [int[,]] $data
hidden [bool[,]] $mask
Grid([int]$width,[int]$height){
$this.mask = [bool[,]]::new($width, $height)
$this.data = [int[,]]::new($width, $height)
}
[int]
Get([int]$x,[int]$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
return $this.data[$x,$y]
}
Set([int]$x,[int]$y,[int]$value)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
if(-not $this.mask[$x,$y])
{
$this.data[$x,$y] = $value
}
else
{
throw [System.InvalidOperationException]::new("Cell [$x,$y] is currently frozen")
}
}
Freeze([int]$x,[int]$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
$this.mask[$x,$y] = $true
}
Unfreeze([int]$x,$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
$this.mask[$x,$y] = $false
}
hidden [bool]
CheckBounds([int]$x,[int]$y)
{
return (
$x -ge $this.data.GetLowerBound(0) -and
$x -le $this.data.GetUpperBound(0) -and
$y -ge $this.data.GetLowerBound(1) -and
$y -le $this.data.GetUpperBound(1)
)
}
}
Now you can do:
$grid = [Grid]::new(5,5)
$grid.Set(0, 0, 1) # Set cell value
$grid.Get(0, 0) # Get cell value
$grid.Freeze(0, 0) # Now freeze cell 0,0
$grid.Set(0, 0, 2) # ... and this will now throw an exception
$grid.Set(0, 1, 1) # Setting any other cell still works
If you want native support for index expressions (ie. $grid[0,0]), the Grid class will need to implement System.Collections.IList

Search class based data structure like $xml.SelectNodes()

I am refactoring some functional code to use classes, and trying to understand the "best" (most performant, easiest to read, least likely to be deprecated, etc.) way to do so.
The functional code uses XML as a data structure, and I need to be able to search for nodes based on certain criteria. So, in simplified form...
$xml = [XML]#"
<Definitions>
<Package id="A">
<Task id="A.1">
<DisplayName>Ignore</DisplayName>
</Task>
<Task id="A.2">
<DisplayName>Find</DisplayName>
</Task>
</Package>
<Package id="B">
<Task id="B.1">
<DisplayName>Ignore</DisplayName>
</Task>
<Task id="B.2">
<DisplayName>Find</DisplayName>
</Task>
<Task id="B.3">
<DisplayName>Ignore</DisplayName>
</Task>
</Package>
<Package id="C">
<Task id="C.1">
<DisplayName>Ignore</DisplayName>
</Task>
<Task id="C.2">
<DisplayName>Ignore</DisplayName>
</Task>
<Task id="C.3">
<DisplayName>Find</DisplayName>
</Task>
</Package>
</Definitions>
"#
$target = 'Find'
$finds = $xml.SelectNodes("//Task/DisplayName[.='$target']")
foreach ($find in $finds) {
Write-Host "$($find.ParentNode.id)"
}
I have managed to get Package and Task collections working, and I can populate a collection of Packages, containing collections of Tasks, based on the same XML, like so...
class Package {
# Properties
[String]$ID
[System.Collections.Generic.List[object]]$Tasks = [System.Collections.Generic.List[object]]::New()
# Constructor
Package ([String]$id) {
$this.ID = $id
}
# Method
[Void] addTask([Task]$newTask) {
$this.Tasks.Add($newTask)
}
}
class Task {
# Properties
[String]$ID
[String]$DisplayName
# Constructor
Task ([String]$id) {
$this.ID = $id
}
}
$definitions = [System.Collections.Generic.List[object]]::New()
foreach ($package in $xml.SelectNodes("//Package")) {
$newPackage = [Package]::New($package.ID)
foreach ($task in $package.SelectNodes("Task")) {
$newTask = [Task]::New($task.ID)
$newTask.DisplayName = $task.DisplayName
$newPackage.addTask($newTask)
}
$definitions.Add($newPackage)
}
And I can verify that is working with some foreach loops...
foreach ($package in $definitions) {
Write-Host "$($package.ID)"
foreach ($task in $package.Tasks) {
Write-Host " $($task.ID) $($task.DisplayName)"
}
}
Now I want to replicate $finds = $xml.SelectNodes("//Task/DisplayName[.='$target']"), and do it the "right" way. I could just iterate through the list like this.
foreach ($package in $definitions) {
foreach ($task in $package.Tasks) {
if ($task.DisplayName -eq $target) {
Write-Host "$($task.ID)"
}
}
}
Or I could have a Find method in the Package class that takes $target as an argument, and iterates over it's own Tasks.
But I wonder if there is some Automatic Variable that already contains all objects of a particular Type, or a way to populate a variable with all objects of a particular Type, so I am iterating over a smaller list. But then I need to be able to find the Parent, and at that point this whole line of thinking seems to break down, since there is no Parent data unless I provide it.
So, what is the best way to do this search?
Also, FWIW, the reason for this exercise is that fact that there are actually about 30 different variations of Task that I will need to implement, with LOTS of shared behavior. Doing that in Functions has led to a bunch of redundant code and lots of work implementing new tasks or fixing bugs in the duplicated code. Inheritance will fix that, and a bunch of other issues that have come up, so moving to classes makes a lot of sense in the bigger picture.
As Theo alludes to in the comments, for a simple object hierarchy like your Package, you can just use Where-Object:
$package.Tasks |Where-Object { $_.DisplayName -eq $target }
Implementing a FindTasks() method is therefore just a question of wrapping Where-Object:
class Package
{
# ...
[Task[]]
FindTasks([scriptblock]$filter)
{
return $this.Tasks |Where-Object $filter
}
}
After which the user can do $package.FindTasks({$_.ID -eq $ID -or $_.DisplayName -like '*keyword*'}) or whatever else they want to filter on

Build Once, Deploy Everywhere with Web Deploy and .NET Web.configs

I am working on setting up a continuous build and deploy system that will manage builds and deployments of our .NET applications to multiple environments. We want to do this so that we build, deploy that build to our development environment, and at a later time have the option to deploy that same build to our test environment with different config file settings. Currently our developers are used to using web.config transforms to manage the config values for each environment and they would prefer to continue doing it that way. Finally, we want to do our deployments with MS Web Deploy 3.6 and its package deploy option.
After doing some research we have found and considered the following options:
Use the Web Deploy parameterization feature to change the config files at deploy time. This would replace the web.config transformations which we would like to avoid.
Run MSBuild once per project configuration/web.config transform to produce a package containing a transformed web.config for each environment. This has the downside of increasing build times and storage requirements for our packages.
Use both Web Deploy parameterization and web.config transformations. This allows developers to continue to use web.configs to debug other environments as they have been and avoids creating multiple packages, but requires us to maintain config settings in multiple places.
At build time, use the web.config transformations to generate multiple config files but only one package and at deploy time use a script to insert the proper config into the correct location of in the package. This seems easier said than done since it's not the way Web Deploy was designed to work and our on initial evaluation appears complicated to implement.
Are there any other options that we haven't considered? Is there a way to do this that allows us to keep using web.configs as we have been but only generate a single Web Deploy package?
In .NET 4.7.1, another option is possible: using ConfigurationBuilder.
The idea is that a custom class has the opportunity to manipulate the values contained in the web.config before they are passed to the application. This allows to plug in other configuration systems.
For example: Using a similar approach to configuration as ASP.NET Core, the NuGet packages it includes can be used independently on .NET Framework as well to load json and override json files. Then an environment variable (or any other value like IIS app pool ID, machine name etc.) can be used to determine which override json file to use.
For example: If there was an appsettings.json file like
{
"appSettings": { "Foo": "FooValue", "Bar": "BarValue" }
}
and an appsettings.Production.json file containing
{
"appSettings": { "Foo": "ProductionFooValue" }
}
One could write a config builder like
public class AppSettingsConfigurationBuilder : ConfigurationBuilder
{
public override ConfigurationSection ProcessConfigurationSection(ConfigurationSection configSection)
{
if(configSection is AppSettingsSection appSettingsSection)
{
var appSettings = appSettingsSection.Settings;
var environmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production";
var appConfig = new ConfigurationBuilder()
.AddJsonFile("appsettings.json", optional: false)
.AddJsonFile($"appsettings.{environmentName}.json", optional: true)
.Build();
appSettings.Add("Foo", appConfig["appSettings:Foo"]);
appSettings.Add("Bar", appConfig["appSettings:Bar"]);
}
return configSection;
}
}
and then wire up the config builder in Web.config:
<configSections>
<section name="configBuilders" type="System.Configuration.ConfigurationBuildersSection, System.Configuration, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" restartOnExternalChanges="false" requirePermission="false"/>
</configSections>
<configBuilders>
<builders>
<add name="AppSettingsConfigurationBuilder" type="My.Project.AppSettingsConfigurationBuilder, My.Project"/>
</builders>
</configBuilders>
<appSettings configBuilders="AppSettingsConfigurationBuilder" />
If you then set the ASPNETCORE_ENVIRONMENT (name only chosen so it ASP.NET Core Apps on the same server would use the same be default) environment variable to Development on dev machines, ConfigurationManager.AppSettings["Foo"] would see FooValue instead of FooProductionValue.
You could also use the APP_POOL_ID to hardcode environment names or use the IIS 10 feature to set environment variables on app pools. This way you can truly build once and copy the same output to different servers or even to multiple directories on the same server and still use a different config for different server.
I don't know if it's less complicated than option 4 above, but the solution we are going with is to run a PowerShell script immediately prior to running MSBuild which parses the web.config transforms and either generates or augments the parameters.xml file. This gives us the flexibility to use parameterization and it's ability to modify config files other than the web.config while preserving 100% of the current functionality of the web.config transformations. Here is the script we are currently using for the benefit of future seekers:
function Convert-XmlElementToString
{
[CmdletBinding()]
param([Parameter(Mandatory=$true)] $xml, [String[]] $attributesToExclude)
$attributesToRemove = #()
foreach($attr in $xml.Attributes) {
if($attr.Name.Contains('xdt') -or $attr.Name.Contains('xmlns') -or $attributesToExclude -contains $attr.Name) {
$attributesToRemove += $attr
}
}
foreach($attr in $attributesToRemove) { $removedAttr = $xml.Attributes.Remove($attr) }
$sw = New-Object System.IO.StringWriter
$xmlSettings = New-Object System.Xml.XmlWriterSettings
$xmlSettings.ConformanceLevel = [System.Xml.ConformanceLevel]::Fragment
$xmlSettings.Indent = $true
$xw = [System.Xml.XmlWriter]::Create($sw, $xmlSettings)
$xml.WriteTo($xw)
$xw.Close()
return $sw.ToString()
}
function BuildParameterXml ($name, $match, $env, $value, $parameterXmlDocument)
{
$existingNode = $parameterXmlDocument.selectNodes("//parameter[#name='$name']")
$value = $value.Replace("'","&apos;") #Need to make sure any single quotes in the value don't break XPath
if($existingNode.Count -eq 0){
#no existing parameter for this transformation
$newParamter = [xml]("<parameter name=`"" + $name + "`">" +
"<parameterEntry kind=`"XmlFile`" scope=`"\\web.config$`" match=`"" + $match + "`" />" +
"<parameterValue env=`"" + $env + "`" value=`"`" />" +
"</parameter>")
$newParamter.selectNodes('//parameter/parameterValue').ItemOf(0).SetAttribute('value', $value)
$imported=$parameterXmlDocument.ImportNode($newParamter.DocumentElement, $true)
$appendedNode = $parameterXmlDocument.selectNodes('//parameters').ItemOf(0).AppendChild($imported)
} else {
#parameter exists but entry is different from an existing entry
$entryXPath = "//parameter[#name=`"$name`"]/parameterEntry[#kind=`"XmlFile`" and #scope=`"\\web.config$`" and #match=`"$match`"]"
$existingEntry = $parameterXmlDocument.selectNodes($entryXPath)
if($existingEntry.Count -eq 0) { throw "There is web.config transformation ($name) that conflicts with an existing parameters.xml entry" }
#parameter exists but environment value is different from an existing environment value
$envValueXPath = "//parameter[#name='$name']/parameterValue[#env='$env' and #value='$value']"
$existingEnvValue = $parameterXmlDocument.selectNodes($envValueXPath)
$existingEnv = $parameterXmlDocument.selectNodes("//parameter[#name=`"$name`"]/parameterValue[#env=`"$env`"]")
if($existingEnvValue.Count -eq 0 -and $existingEnv.Count -gt 0) {
throw "There is web.config transformation ($name) for this environment ($env) that conflicts with an existing parameters.xml value"
} elseif ($existingEnvValue.Count -eq 0 -and $existingEnv.Count -eq 0) {
$newParamter = [xml]("<parameterValue env=`"" + $env + "`" value=`"`" />")
$newParamter.selectNodes('//parameterValue').ItemOf(0).SetAttribute('value', $value)
$imported=$parameterXmlDocument.ImportNode($newParamter.DocumentElement, $true)
$appendedNode = $existingNode.ItemOf(0).AppendChild($imported)
}
}
}
function UpdateSetParams ($node, $originalXml, $path, $env, $parametersXml)
{
foreach ($childNode in $node.ChildNodes)
{
$xdtValue = ""
$name = ""
$match = ($path + $childNode.toString())
if($childNode.Attributes -and $childNode.Attributes.GetNamedItem('xdt:Locator').Value) {
$hasMatch = $childNode.Attributes.GetNamedItem('xdt:Locator').Value -match ".?\((.*?)\).*"
$name = $childNode.Attributes.GetNamedItem($matches[1]).Value
$match = $match + "[#" + $matches[1] + "=`'" + $name + "`']"
}
if($childNode.Attributes -and $childNode.Attributes.GetNamedItem('xdt:Transform')) {
$xdtValue = $childNode.Attributes.GetNamedItem('xdt:Transform').Value
}
if($xdtValue -eq 'Replace') {
if($childNode.Attributes.GetNamedItem('xdt:Locator').Value) {
$hasMatch = $childNode.Attributes.GetNamedItem('xdt:Locator').Value -match ".?\((.*?)\).*"
$name = $childNode.Attributes.GetNamedItem($matches[1]).Value
} else {
$name = $childNode.toString()
}
$nodeString = Convert-XmlElementToString $childNode.PsObject.Copy()
BuildParameterXml $name $match $env $nodeString $parametersXml
} elseif ($xdtValue.Contains('RemoveAttributes')) {
if($originalXml.selectNodes($match).Count -gt 0) {
$hasMatch = $xdtValue -match ".?\((.*?)\).*"
$nodeString = Convert-XmlElementToString $originalXml.selectNodes($match).ItemOf(0).PsObject.Copy() $matches[1].Split(',')
$newParamter = BuildParameterXml $childNode.toString() $match $env $nodeString $parametersXml
$newParamters += $newParamter
}
} elseif ($xdtValue.Contains('SetAttributes')) {
if($originalXml.selectNodes($match).Count -gt 0) {
$nodeCopy = $originalXml.selectNodes($match).ItemOf(0).PsObject.Copy()
$hasMatch = $xdtValue -match ".?\((.*?)\).*"
foreach($attr in $matches[1].Split(',')){
$nodeCopy.SetAttribute($attr, $childNode.Attributes.GetNamedItem($attr).Value)
}
$nodeString = Convert-XmlElementToString $nodeCopy
BuildParameterXml $childNode.toString() "($match)[1]" $env $nodeString $parametersXml
}
} elseif ($xdtValue) {
throw "Yikes! the script doesn't know how to handle this transformation!"
}
#Recurse into this node to check if it has transformations on its children
if($childNode) {
UpdateSetParams $childNode $originalXml ($match + "/") $env $parametersXml
}
}
}
function TransformConfigsIntoParamters ($webConfigPath, $webConfigTransformPath, $parametersXml)
{
#Parse out the environment names
$hasMatch = $webConfigTransformPath -match ".?web\.(.*?)\.config.*"
[xml]$transformXml = Get-Content $webConfigTransformPath
[xml]$webConfigXml = Get-Content $webConfigPath
UpdateSetParams $transformXml $webConfigXml '//' $matches[1] $parametersXml
}
$applicationRoot = $ENV:WORKSPACE
if(Test-Path ($applicationRoot + '\parameters.xml')) {
[xml]$parametersXml = Get-Content ($applicationRoot + '\parameters.xml')
$parametersNode = $parametersXml.selectNodes('//parameters').ItemOf(0)
} else {
[System.XML.XMLDocument]$parametersXml=New-Object System.XML.XMLDocument
[System.XML.XMLElement]$parametersNode=$parametersXml.CreateElement("parameters")
$appendedNode = $parametersXml.appendChild($parametersNode)
}
TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.Development.config') $parametersXml
TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.SystemTest.config') $parametersXml
TransformConfigsIntoParamters ($applicationRoot + '\web.config') ($applicationRoot + '\web.Production.config') $parametersXml
$parametersXml.Save($applicationRoot + '\parameters.xml')

SeleniumRC/Perl dynamic XPath selector not working

This is more a question for XPath syntax than anything else.
I have multiple product pages on a site that have multiple products on each product pages. Each product has a unique ID for the add-to-cart button. I'm trying to return all of the unique ID's so that I can add a couple of the products to the bag. Searching with XPath seems to be the correct solution for this. I have the following code for querying the HTML with XPath and returning the unique ID's:
$XPATH_COUNT = $sel->get_xpath_count("//div[\#class='quick-info-link']/a");
#my_array;
$my_array[0] = $sel->get_attribute("//div[\#class='quick-info-link']/a/\#id");
print $my_array[0];
$count = 0;
while( $count < $XPATH_COUNT )
{
$arrayCount=0;
$a = "//";
foreach( #my_array )
{
$tmp = "a[\#id!='" . $my_array[$arrayCount] . "' and ";
$b .= $tmp;
$d .= "]";
$arrayCount++;
}
$c = "img[\#alt='Quick Shop']";
$e = $c . $d . "/\#id";
$xpath_query = $a . $b . $e;
$my_array[$count] = $sel->get_attribute($xpath_query);
$count++;
}
The output of the first run of this is an XPath query that looks like this:
//a[#id!='quickview-link-PROD7029' and img[#alt='Quick Shop']]/#id
Which correctly returns quickview-link-PROD6945. The second run produces this:
//a[#id!='quickview-link-PROD7029' and a[#id!='quickview-link-PROD6945' and img[#alt='Quick Shop']]]/#id
Which throws an error in my SeleniumRC terminal window of ERROR: Element [..xpath query..] not found on session.
I am aware of the possible use of indexes (i.e. adding an [i] to the end of the XPath query) to access elements on the page, however this isn't something that has worked for me in Selenium.
Any help would be great. Thanks for your time,
Steve
//a[#id!='quickview-link-PROD7029'
and a[#id!='quickview-link-PROD6945' and
img[#alt='Quick Shop']
]
]/#id
Which throws an error in my SeleniumRC
terminal window of ERROR: Element
[..xpath query..] not found on session
It would greatly help if you provide the XML document on which the XPath expression is applied and explain which node(s) you want to select.
Without this necessary information:
The most obvious reason for this problem is that the above expression is looking for a elements that have an a child with some property.
Usually an a element doesn't have any a children.
What you really want is something like:
//a[#id != 'quickview-link-PROD7029'
and
#id != 'quickview-link-PROD6945' and img[#alt='Quick Shop']
]/#id
This can be simplified a bit:
//a[img[#alt='Quick Shop']/#id
[not(. = 'quickview-link-PROD7029'
or
. = 'quickview-link-PROD6945'
)
]