How to use node_filter method of Capybara Package

Best Capybara code snippet using Capybara.node_filter

selector.rb

Source:selector.rb Github

copy

Full Screen

1# frozen_string_literal: true2require 'capybara/selector/selector'3Capybara::Selector::FilterSet.add(:_field) do4 node_filter(:checked, :boolean) { |node, value| !(value ^ node.checked?) }5 node_filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }6 node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }7 node_filter(:multiple, :boolean) { |node, value| !(value ^ node.multiple?) }8 expression_filter(:name) { |xpath, val| xpath[XPath.attr(:name) == val] }9 expression_filter(:placeholder) { |xpath, val| xpath[XPath.attr(:placeholder) == val] }10 describe(:node_filters) do |checked: nil, unchecked: nil, disabled: nil, multiple: nil, **|11 desc, states = +'', []12 states << 'checked' if checked || (unchecked == false)13 states << 'not checked' if unchecked || (checked == false)14 states << 'disabled' if disabled == true15 states << 'not disabled' if disabled == false16 desc << " that is #{states.join(' and ')}" unless states.empty?17 desc << ' with the multiple attribute' if multiple == true18 desc << ' without the multiple attribute' if multiple == false19 desc20 end21end22# rubocop:disable Metrics/BlockLength23Capybara.add_selector(:xpath) do24 xpath { |xpath| xpath }25end26Capybara.add_selector(:css) do27 css { |css| css }28end29Capybara.add_selector(:id) do30 xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }31end32Capybara.add_selector(:field) do33 xpath do |locator, **options|34 xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of('submit', 'image', 'hidden')]35 locate_field(xpath, locator, options)36 end37 expression_filter(:type) do |expr, type|38 type = type.to_s39 if %w[textarea select].include?(type)40 expr.self(type.to_sym)41 else42 expr[XPath.attr(:type) == type]43 end44 end45 filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder46 node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }47 node_filter(:with) do |node, with|48 with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s49 end50 describe_expression_filters do |type: nil, **options|51 desc = +''52 (expression_filters.keys - [:type]).each { |ef| desc << " with #{ef} #{options[ef]}" if options.key?(ef) }53 desc << " of type #{type.inspect}" if type54 desc55 end56 describe_node_filters do |**options|57 " with value #{options[:with].to_s.inspect}" if options.key?(:with)58 end59end60Capybara.add_selector(:fieldset) do61 xpath(:legend) do |locator, legend: nil, **|62 locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]63 locator_matchers |= XPath.attr(test_id) == locator if test_id64 xpath = XPath.descendant(:fieldset)65 xpath = xpath[locator_matchers] unless locator.nil?66 xpath = xpath[XPath.child(:legend)[XPath.string.n.is(legend)]] if legend67 xpath68 end69 node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }70end71Capybara.add_selector(:link) do72 xpath(:title, :alt) do |locator, href: true, alt: nil, title: nil, **|73 xpath = XPath.descendant(:a)74 xpath = xpath[75 case href76 when nil, false77 !XPath.attr(:href)78 when true79 XPath.attr(:href)80 when Regexp81 nil # needs to be handled in filter82 else83 XPath.attr(:href) == href.to_s84 end85 ]86 unless locator.nil?87 locator = locator.to_s88 matchers = [XPath.attr(:id) == locator,89 XPath.string.n.is(locator),90 XPath.attr(:title).is(locator),91 XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]92 matchers << XPath.attr(:'aria-label').is(locator) if enable_aria_label93 matchers << XPath.attr(test_id) == locator if test_id94 xpath = xpath[matchers.reduce(:|)]95 end96 xpath = xpath[find_by_attr(:title, title)]97 xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt) == alt]] if alt98 xpath99 end100 node_filter(:href) do |node, href|101 # If not a Regexp it's been handled in the main XPath102 href.is_a?(Regexp) ? node[:href].match(href) : true103 end104 expression_filter(:download, valid_values: [true, false, String]) do |expr, download|105 mod = case download106 when true then XPath.attr(:download)107 when false then !XPath.attr(:download)108 when String then XPath.attr(:download) == download109 end110 expr[mod]111 end112 describe_expression_filters do |**options|113 desc = +''114 desc << " with href #{options[:href].inspect}" if options[:href] && !options[:href].is_a?(Regexp)115 desc << ' with no href attribute' if options.fetch(:href, true).nil?116 desc117 end118 describe_node_filters do |href: nil, **|119 " with href matching #{href.inspect}" if href.is_a? Regexp120 end121end122Capybara.add_selector(:button) do123 xpath(:value, :title, :type) do |locator, **options|124 input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]125 btn_xpath = XPath.descendant(:button)126 image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']127 unless locator.nil?128 locator = locator.to_s129 locator_matchers = XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)130 locator_matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label131 locator_matchers |= XPath.attr(test_id) == locator if test_id132 input_btn_xpath = input_btn_xpath[locator_matchers]133 btn_xpath = btn_xpath[locator_matchers | XPath.string.n.is(locator) | XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]134 alt_matches = XPath.attr(:alt).is(locator)135 alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label136 image_btn_xpath = image_btn_xpath[alt_matches]137 end138 res_xpath = input_btn_xpath.union(btn_xpath).union(image_btn_xpath)139 res_xpath = expression_filters.keys.inject(res_xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }140 res_xpath141 end142 node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }143 describe_expression_filters144 describe_node_filters do |disabled: nil, **|145 ' that is disabled' if disabled == true146 end147end148Capybara.add_selector(:link_or_button) do149 label 'link or button'150 xpath do |locator, **options|151 self.class.all.values_at(:link, :button).map { |selector| selector.xpath.call(locator, options) }.reduce(:union)152 end153 node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| node.tag_name == 'a' || !(value ^ node.disabled?) }154 describe_node_filters do |disabled: nil, **|155 ' that is disabled' if disabled == true156 end157end158Capybara.add_selector(:fillable_field) do159 label 'field'160 xpath(:allow_self) do |locator, **options|161 xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input, :textarea)[162 !XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')163 ]164 locate_field(xpath, locator, options)165 end166 expression_filter(:type) do |expr, type|167 type = type.to_s168 if type == 'textarea'169 expr.self(type.to_sym)170 else171 expr[XPath.attr(:type) == type]172 end173 end174 filter_set(:_field, %i[disabled multiple name placeholder])175 node_filter(:with) do |node, with|176 with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s177 end178 describe_expression_filters179 describe_node_filters do |**options|180 " with value #{options[:with].to_s.inspect}" if options.key?(:with)181 end182end183Capybara.add_selector(:radio_button) do184 label 'radio button'185 xpath(:allow_self) do |locator, **options|186 xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input)[187 XPath.attr(:type) == 'radio'188 ]189 locate_field(xpath, locator, options)190 end191 filter_set(:_field, %i[checked unchecked disabled name])192 node_filter(:option) { |node, value| node.value == value.to_s }193 describe_expression_filters194 describe_node_filters do |option: nil, **|195 " with value #{option.inspect}" if option196 end197end198Capybara.add_selector(:checkbox) do199 xpath(:allow_self) do |locator, **options|200 xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input)[201 XPath.attr(:type) == 'checkbox'202 ]203 locate_field(xpath, locator, options)204 end205 filter_set(:_field, %i[checked unchecked disabled name])206 node_filter(:option) { |node, value| node.value == value.to_s }207 describe_expression_filters208 describe_node_filters do |option: nil, **|209 " with value #{option.inspect}" if option210 end211end212Capybara.add_selector(:select) do213 label 'select box'214 xpath do |locator, **options|215 xpath = XPath.descendant(:select)216 locate_field(xpath, locator, options)217 end218 filter_set(:_field, %i[disabled multiple name placeholder])219 node_filter(:options) do |node, options|220 actual = if node.visible?221 node.all(:xpath, './/option', wait: false).map(&:text)222 else223 node.all(:xpath, './/option', visible: false, wait: false).map { |option| option.text(:all) }224 end225 options.sort == actual.sort226 end227 expression_filter(:with_options) do |expr, options|228 options.inject(expr) do |xpath, option|229 xpath[Capybara::Selector.all[:option].call(option)]230 end231 end232 node_filter(:selected) do |node, selected|233 actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }234 Array(selected).sort == actual.sort235 end236 node_filter(:with_selected) do |node, selected|237 actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }238 (Array(selected) - actual).empty?239 end240 describe_expression_filters do |with_options: nil, **opts|241 desc = +''242 desc << " with at least options #{with_options.inspect}" if with_options243 desc << describe_all_expression_filters(opts)244 desc245 end246 describe_node_filters do |options: nil, selected: nil, with_selected: nil, **|247 desc = +''248 desc << " with options #{options.inspect}" if options249 desc << " with #{selected.inspect} selected" if selected250 desc << " with at least #{with_selected.inspect} selected" if with_selected251 desc252 end253end254Capybara.add_selector(:datalist_input) do255 label 'input box with datalist completion'256 xpath do |locator, **options|257 xpath = XPath.descendant(:input)[XPath.attr(:list)]258 locate_field(xpath, locator, options)259 end260 filter_set(:_field, %i[disabled name placeholder])261 node_filter(:options) do |node, options|262 actual = node.find("//datalist[@id=#{node[:list]}]", visible: :all).all(:datalist_option, wait: false).map(&:value)263 options.sort == actual.sort264 end265 expression_filter(:with_options) do |expr, options|266 options.inject(expr) do |xpath, option|267 xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[Capybara::Selector.all[:datalist_option].call(option)].attr(:id)]268 end269 end270 describe_expression_filters do |with_options: nil, **opts|271 desc = +''272 desc << " with at least options #{with_options.inspect}" if with_options273 desc << describe_all_expression_filters(opts)274 desc275 end276 describe_node_filters do |options: nil, **|277 " with options #{options.inspect}" if options278 end279end280Capybara.add_selector(:option) do281 xpath do |locator|282 xpath = XPath.descendant(:option)283 xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?284 xpath285 end286 node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }287 node_filter(:selected, :boolean) { |node, value| !(value ^ node.selected?) }288 describe_node_filters do |**options|289 desc = +''290 desc << " that is#{' not' unless options[:disabled]} disabled" if options.key?(:disabled)291 desc << " that is#{' not' unless options[:selected]} selected" if options.key?(:selected)292 desc293 end294end295Capybara.add_selector(:datalist_option) do296 label 'datalist option'297 visible(:all)298 xpath do |locator|299 xpath = XPath.descendant(:option)300 xpath = xpath[XPath.string.n.is(locator.to_s) | (XPath.attr(:value) == locator.to_s)] unless locator.nil?301 xpath302 end303 node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }304 describe_node_filters do |**options|305 " that is#{' not' unless options[:disabled]} disabled" if options.key?(:disabled)306 end307end308Capybara.add_selector(:file_field) do309 label 'file field'310 xpath(:allow_self) do |locator, **options|311 xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input)[312 XPath.attr(:type) == 'file'313 ]314 locate_field(xpath, locator, options)315 end316 filter_set(:_field, %i[disabled multiple name])317 describe_expression_filters318end319Capybara.add_selector(:label) do320 label 'label'321 xpath(:for) do |locator, options|322 xpath = XPath.descendant(:label)323 unless locator.nil?324 locator_matchers = XPath.string.n.is(locator.to_s) | (XPath.attr(:id) == locator.to_s)325 locator_matchers |= XPath.attr(test_id) == locator if test_id326 xpath = xpath[locator_matchers]327 end328 if options.key?(:for) && !options[:for].is_a?(Capybara::Node::Element)329 with_attr = XPath.attr(:for) == options[:for].to_s330 labelable_elements = %i[button input keygen meter output progress select textarea]331 wrapped = !XPath.attr(:for) &332 XPath.descendant(*labelable_elements)[XPath.attr(:id) == options[:for].to_s]333 xpath = xpath[with_attr | wrapped]334 end335 xpath336 end337 node_filter(:for) do |node, field_or_value|338 if field_or_value.is_a? Capybara::Node::Element339 if node[:for]340 field_or_value[:id] == node[:for]341 else342 field_or_value.find_xpath('./ancestor::label[1]').include? node.base343 end344 else345 true # Non element values were handled through the expression filter346 end347 end348 describe_expression_filters do |**options|349 " for element with id of \"#{options[:for]}\"" if options.key?(:for) && !options[:for].is_a?(Capybara::Node::Element)350 end351 describe_node_filters do |**options|352 " for element #{options[:for]}" if options[:for]&.is_a?(Capybara::Node::Element)353 end354end355Capybara.add_selector(:table) do356 xpath(:caption) do |locator, caption: nil, **|357 xpath = XPath.descendant(:table)358 unless locator.nil?359 locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.descendant(:caption).is(locator.to_s)360 locator_matchers |= XPath.attr(test_id) == locator if test_id361 xpath = xpath[locator_matchers]362 end363 xpath = xpath[XPath.descendant(:caption) == caption] if caption364 xpath365 end366 describe_expression_filters do |caption: nil, **|367 " with caption \"#{caption}\"" if caption368 end369end370Capybara.add_selector(:frame) do371 xpath(:name) do |locator, **options|372 xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))373 unless locator.nil?374 locator_matchers = (XPath.attr(:id) == locator.to_s) | (XPath.attr(:name) == locator.to_s)375 locator_matchers |= XPath.attr(test_id) == locator if test_id376 xpath = xpath[locator_matchers]377 end378 xpath = expression_filters.keys.inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }379 xpath380 end381 describe_expression_filters do |name: nil, **|382 " with name #{name}" if name383 end384end385Capybara.add_selector(:element) do386 xpath do |locator, **|387 locator ? XPath.descendant(locator.to_sym) : XPath.descendant388 end389 expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|390 case val391 when Regexp392 xpath393 when true394 xpath[XPath.attr(name)]395 when false396 xpath[!XPath.attr(name)]397 when XPath::Expression398 xpath[XPath.attr(name)[val]]399 else400 xpath[XPath.attr(name.to_sym) == val]401 end402 end403 node_filter(:attributes, matcher: /.+/) do |node, name, val|404 val.is_a?(Regexp) ? node[name] =~ val : true405 end406 describe_expression_filters do |**options|407 booleans, values = options.partition { |_k, v| [true, false].include? v }.map(&:to_h)408 desc = describe_all_expression_filters(values)409 desc + booleans.map do |k, v|410 v ? " with #{k} attribute" : "without #{k} attribute"411 end.join412 end413end414# rubocop:enable Metrics/BlockLength...

Full Screen

Full Screen

node_filter

Using AI Code Generation

copy

Full Screen

1 Capybara::Selenium::Driver.new(app, :browser => :chrome)2Capybara.node_filter(:visible) do |node|3Capybara.visit('https://www.youtube.com/watch?v=6Qe7rUO8r9c')4sleep(5)5Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|6Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|7Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|8Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|9Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|10Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|11Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|12Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|13Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do

Full Screen

Full Screen

node_filter

Using AI Code Generation

copy

Full Screen

1 Capybara::Selenium::Driver.new(app, :browser => :chrome)2Capybara.node_filter(:visible) do |node|3Capybara.visit('https://www.youtube.com/watch?v=6Qe7rUO8r9c')4sleep(5)5Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|6Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|7Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|8Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|9Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|10Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|11Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|12Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do |x|13Capybara.all(:xpath, '//div[@class="style-scope ytd-video-primary-info-renderer"]', :visible => true).each do

Full Screen

Full Screen

node_filter

Using AI Code Generation

copy

Full Screen

1 Capybara::Poltergeist::Driver.new(app, {2 })3 Capybara::Node::Filter.new(self)4 Capybara::Node::Filter.new(self)5 Capybara::Node::Filter.new(self)6 def initialize(node)7 Capybara::Node::Filter.new(self)

Full Screen

Full Screen

node_filter

Using AI Code Generation

copy

Full Screen

1 Capybara::Poltergeist::Driver.new(app, {2 })3 Capybara::Node::Filter.new(self)

Full Screen

Full Screen

node_filter

Using AI Code Generation

copy

Full Screen

1 Capybara::Selenium::Driver.new(app, :browser => :firefox)2node_filter('input[name="q"]').set('capybara')3 Capybara::Selenium::Driver.new(app, :browser => :firefox)4node_filter('input[name="q"]').set('capybara')5 Capybara::Selenium::Driver.new(app, :browser => :firefox)6node_filter('input[name="q"]').set('capybara')7 Capybara::Selenium::Driver.new(app, :browser => :firefox)

Full Screen

Full Screen

node_filter

Using AI Code Generation

copy

Full Screen

1Dir.glob('*.rb') do |file|2Dir.glob('[0-9]*.rb') do |file|3Dir.glob('*[0-9].rb') do |file|4Dir.glob('[a-z]*[0-95 Capybara::Node::Filter.new(self)6 Capybara::Node::Filter.new(self)7 def initialize(node)8 Capybara::Node::Filter.new(self)

Full Screen

Full Screen

node_filter

Using AI Code Generation

copy

Full Screen

1 @filters = {}2 def add(name, &block)3 def apply(name, node)4 if @filters.include?(name)5 @filters[name].call(node)6Capybara.node_filter.add(:text) do |node|7Capybara.node_filter.add(:link) do |node|8Capybara.node_filter.add(:image) do |node|9Capybara.node_filter.add(:alt) do |node|10Capybara.node_filter.add(:title) do |node|11Capybara.node_filter.add(:class) do |node|12Capybara.node_filter.add(:id) do |node|13Capybara.node_filter.add(:name) do |node|14Capybara.node_filter.add(:value) do |node|15Capybara.node_filter.add(:attribute) do |node|16Capybara.node_filter.add(:css) do |node|17 def [](name)18 attribute(name)

Full Screen

Full Screen

node_filter

Using AI Code Generation

copy

Full Screen

1 Capybara::Selenium::Driver.new(app, :browser => :firefox)2node_filter('input[name="q"]').set('capybara')3 Capybara::Selenium::Driver.new(app, :browser => :firefox)4node_filter('input[name="q"]').set('capybara')5 Capybara::Selenium::Driver.new(app, :browser => :firefox)6node_filter('input[name="q"]').set('capybara')7 Capybara::Selenium::Driver.new(app, :browser => :firefox)

Full Screen

Full Screen

node_filter

Using AI Code Generation

copy

Full Screen

1 @filters = {}2 def add(name, &block)3 def apply(name, node)4 if @filters.include?(name)5 @filters[name].call(node)6Capybara.node_filter.add(:text) do |node|7Capybara.node_filter.add(:link) do |node|8Capybara.node_filter.add(:image) do |node|9Capybara.node_filter.add(:alt) do |node|10Capybara.node_filter.add(:title) do |node|11Capybara.node_filter.add(:class) do |node|12Capybara.node_filter.add(:id) do |node|13Capybara.node_filter.add(:name) do |node|14Capybara.node_filter.add(:value) do |node|15Capybara.node_filter.add(:attribute) do |node|16Capybara.node_filter.add(:css) do |node|17 def [](name)18 attribute(name)

Full Screen

Full Screen

node_filter

Using AI Code Generation

copy

Full Screen

1 Capybara::Poltergeist::Driver.new(app, js_errors: false)2 def search_for(search_term)3 visit('/')4 fill_in('q', with: search_term)5 click_button('Google Search')6 node.find('h3').text.match(/wikipedia/i)7 node.find('h3').text8puts google.search_for('wikipedia')

Full Screen

Full Screen

Automation Testing Tutorials

Learn to execute automation testing from scratch with LambdaTest Learning Hub. Right from setting up the prerequisites to run your first automation test, to following best practices and diving deeper into advanced test scenarios. LambdaTest Learning Hubs compile a list of step-by-step guides to help you be proficient with different test automation frameworks i.e. Selenium, Cypress, TestNG etc.

LambdaTest Learning Hubs:

YouTube

You could also refer to video tutorials over LambdaTest YouTube channel to get step by step demonstration from industry experts.

Run Capybara automation tests on LambdaTest cloud grid

Perform automation testing on 3000+ real desktop and mobile devices online.

Most used method in

Try LambdaTest Now !!

Get 100 minutes of automation test minutes FREE!!

Next-Gen App & Browser Testing Cloud

Was this article helpful?

Helpful

NotHelpful