Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table#rows #40

Merged
merged 11 commits into from
Dec 25, 2012
Binary file modified ext/UiaDll/Release/UiaDll.dll
Binary file not shown.
28 changes: 28 additions & 0 deletions ext/UiaDll/UiaDll/AutomatedTable.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,34 @@ int AutomatedTable::RowCount::get()
return tablePattern->Current.RowCount;
}

bool AutomatedTable::Exists(const char* whichItem)
{
return Exists(gcnew PropertyCondition(AutomationElement::NameProperty, gcnew String(whichItem)));
}

bool AutomatedTable::Exists(const int whichItemIndex)
{
return Exists(gcnew PropertyCondition(TableItemPattern::RowProperty, whichItemIndex));
}

String^ AutomatedTable::ValueAt(const int dataRow)
{
return DataItemAt(dataRow)->Current.Name;
}

AutomationElement^ AutomatedTable::DataItemAt(const int whichItemIndex)
{
auto dataItemProperty = gcnew PropertyCondition(AutomationElement::IsTableItemPatternAvailableProperty, true);
auto indexProperty = gcnew PropertyCondition(TableItemPattern::RowProperty, whichItemIndex);
return _tableControl->FindFirst(System::Windows::Automation::TreeScope::Subtree, gcnew AndCondition(dataItemProperty, indexProperty));
}

bool AutomatedTable::Exists(Condition^ condition)
{
auto dataItemProperty = gcnew PropertyCondition(AutomationElement::IsTableItemPatternAvailableProperty, true);
return _tableControl->FindAll(System::Windows::Automation::TreeScope::Subtree, gcnew AndCondition(dataItemProperty, condition))->Count > 0;
}

void AutomatedTable::Select(const int dataItemIndex)
{
auto dataItemProperty = gcnew PropertyCondition(AutomationElement::ControlTypeProperty, ControlType::DataItem);
Expand Down
6 changes: 5 additions & 1 deletion ext/UiaDll/UiaDll/AutomatedTable.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ ref class AutomatedTable
{
public:
AutomatedTable(const HWND windowHandle);

bool Exists(const char* whichItem);
bool Exists(const int whichItemIndex);
String^ ValueAt(const int whichItemIndex);
void Select(const int dataItemIndex);

property int RowCount {
Expand All @@ -14,5 +16,7 @@ ref class AutomatedTable

private:
AutomationElement^ _tableControl;
bool Exists(Condition^ condition);
AutomationElement^ DataItemAt(const int whichItemIndex);
};

30 changes: 30 additions & 0 deletions ext/UiaDll/UiaDll/UiaDll.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -464,6 +464,36 @@ extern "C" {
}
}

__declspec ( dllexport ) bool RA_DataItemExistsByValue(const HWND windowHandle, const char* whichItem) {
try {
auto tableControl = gcnew AutomatedTable(windowHandle);
return tableControl->Exists(whichItem);
} catch(Exception^ e) {
Console::WriteLine(e->ToString());
return false;
}
}

__declspec ( dllexport ) bool RA_DataItemExistsByIndex(const HWND windowHandle, const int whichItemIndex) {
try {
auto tableControl = gcnew AutomatedTable(windowHandle);
return tableControl->Exists(whichItemIndex);
} catch(Exception^ e) {
Console::WriteLine(e->ToString());
return false;
}
}

__declspec ( dllexport ) void RA_RowValueAt(const HWND windowHandle, const int row, char *foundValue, const int foundValueLength) {
try {
auto tableControl = gcnew AutomatedTable(windowHandle);
auto rowValue = tableControl->ValueAt(row);
StringHelper::CopyToUnmanagedString(rowValue, foundValue, foundValueLength);
} catch(Exception^ e) {
Console::WriteLine(e->ToString());
}
}

__declspec ( dllexport ) void RA_SelectDataItem(const HWND windowHandle, const int dataItemIndex) {
try {
auto tableControl = gcnew AutomatedTable(windowHandle);
Expand Down
44 changes: 44 additions & 0 deletions lib/rautomation/adapter/ms_uia/table.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,53 @@
module RAutomation
module Adapter
module MsUia
class Row
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As i understand then subclassing Control in this case wouldn't work?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should Row be a Control? SelectList::SelectListOption is not a Control. Should this be different?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure, see if there's anything you could reuse from Control. It doesn't have to be that, but if it makes sense then i don't see any reason, why not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, I'll check into it. I most likely won't get to it until midnight tonight (I'm -5 GMT).

include Locators

def initialize(window, locators)
@hwnd = window.hwnd
@locators = extract(locators)
end

def index
@locators[:index] || 0
end

def value
UiaDll::row_value_at @hwnd, @locators[:index]
end

def exists?
UiaDll::data_item_exists_by_index(@hwnd, @locators[:index])
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is #exists? working with other locators than :index?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, currently it only works with the :index locator. With these first commits (for Table#Rows and Row#Cells) I just wanted to get the minimal in there to make it conform to the ElementCollection. I planned on adding support for other locators after this was pulled in.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, right.

end

alias_method :text, :value
alias_method :row, :index
end

class Table < Control
include WaitHelper
include Locators
extend ElementCollections

has_many :rows

def row(locators={})
rows(locators).first
end

def rows(locators={})
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If Row#exists? would check for all locators and not just index, then you could delete this method, since has_many already creates it and checks against #exists?.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I will make that change

Rows.new(self, locators).select do |row|
locators_match? locators, row
end
end

def locators_match?(locators, row)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this into Row and use by #exists? as mentioned above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After looking at the code again, I now remember why I did it this way. I did actually add the ability to filter by both text and by RegEx, but I did now know how to get this to work unless I retrieved all of the rows from the UiaDll.dll first. The way the ElementCollection code works is to break out of the loop in the #each method if exists? fails. Having seen that, I did not know of a way to filter the rows out without retrieving all of them first. I could have it filter by the exact text in such a manner, but the RegEx one would be tricky. Let me know how you'd like for me to proceed.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, what this #exists? normally does it will try to locate an element with specified locators by increasing index. If that element is not found, then it will break out of the loop since that means there's no more matching controls.

Can't you do something similar? Instead of doing data_item_exists_by_index, can't you do something like match_all_locators? data_items[locators[:index]]?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, but as soon as it finds one that does not match at all it quits. If you were doing a /hello/ and you had the following items in a Table:

  • This would match because hello is in it
  • helllllllllo
  • hello2

It would only find the first one (rather than the 1st and the 3rd), correct? I'm sorry if I'm being obtuse about this, I just didn't know how to get this to check all of them, and not quit once something did not match.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since i do not know the inner workings of UiaDll, then can't you just retrieve all the data item controls and then perform the matching at Ruby side one by one?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's exactly what this is doing.

  Rows.new(self, locators)

If you specified an :index in the locators, it would only grab that one. If you did not, ElementCollection keeps grabbing all of them. So after the above executes, you have all of the rows. After that, then it applies the other locators and only includes those that match with a Enumerable#select:

 Rows.new(self, locators).select { |row| match_all_other_locators_besides_index?(row, locators) } 

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just don't like the fact that Rows.new is used inside of Row. I'd like if that would work like other Controls - i can imagine that this would be only possible if somehow uiadll would return a collection of all rows which can be iterated later on. In that case the solution i'm looking for would work.

Nevertheless - i'm merging this now, maybe you or someone else who knows these things technically can come up with better solution.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you were happy with only supporting :text and :index locators, this could be done in UiaDll. I was thinking of doing this anyway, because if you had a ListView with a ton of rows it could be expensive to grab all of the items every time.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you imagine any other possible locator for this kind of controls which would actually make sense? If not, then :text and :index seem to be sufficient.

Anyway, merged changes in for now, so let's iterate in the future :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:text and :index seem sufficient to me. I will revisit this as I use it more :-)

locators.all? do |locator, value|
return row.value =~ value if value.is_a? Regexp
return row.send(locator) == value
end
end

def strings
rows = []
Expand Down
13 changes: 13 additions & 0 deletions lib/rautomation/adapter/ms_uia/uia_dll.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ module UiaDll
[:long], :int
attach_function :select_data_item, :RA_SelectDataItem,
[:long, :int], :void
attach_function :data_item_exists_by_value, :RA_DataItemExistsByValue,
[:long, :string], :bool
attach_function :data_item_exists_by_index, :RA_DataItemExistsByIndex,
[:long, :int], :bool

attach_function :RA_RowValueAt,
[:long, :int, :pointer, :int], :void

def self.row_value_at(hwnd, which_index)
string = FFI::MemoryPointer.new :char, 1024
RA_RowValueAt hwnd, which_index, string, 1024
string.read_string
end
end
end
end
Expand Down
44 changes: 44 additions & 0 deletions spec/adapter/ms_uia/table_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,49 @@
table.row_count.should eq(2)
end

context "#rows" do
let(:table) { RAutomation::Window.new(:title => "DataEntryForm").table(:id => "personListView") }

it "has rows" do
table.rows.size.should eq 2
end

it "have values" do
table.rows.map(&:value).should eq ["John Doe", "Anna Doe"]
end

it "values are also text" do
table.rows.map(&:text).should eq ["John Doe", "Anna Doe"]
end

context "locators" do
it "can locate by text" do
table.rows(:text => "Anna Doe").size.should eq 1
end

it "can locate by regex" do
table.rows(:text => /Doe/).size.should eq 2
end

it "can locate by index" do
table.rows(:index => 1).first.text.should eq "Anna Doe"
end

it "an index is also a row" do
table.rows(:row => 1).first.text.should eq "Anna Doe"
end
end

context "singular row" do
it "grabs the first by default" do
table.row.text.should eq "John Doe"
end

it "can haz locators too" do
table.row(:text => "Anna Doe").text.should eq "Anna Doe"
end
end
end

end