Class: Capybara::Playwright::Node

Inherits:
Driver::Node
  • Object
show all
Defined in:
lib/capybara/playwright/node.rb

Overview

Selector and checking methods are derived from twapole/apparition Action methods (click, select_option, ...) uses playwright.

ref:

selenium:   https://github.com/teamcapybara/capybara/blob/master/lib/capybara/selenium/node.rb
apparition: https://github.com/twalpole/apparition/blob/master/lib/capybara/apparition/node.rb

Direct Known Subclasses

ShadowRootNode

Defined Under Namespace

Modules: UpdateValueJS Classes: Checkbox, ClickOptions, DateInput, DateTimeInput, DragTo, FileUpload, JSValueInput, NotActionableError, RadioButton, SendKeys, Settable, StaleReferenceError, TextInput, TimeInput

Constant Summary collapse

ATTACH_FILE =
<<~JAVASCRIPT
  () => {
    const input = document.createElement('INPUT');
    input.type = 'file';
    input.multiple = true;
    input.style.display = 'none';
    document.body.appendChild(input);
    return input;
  }
JAVASCRIPT
DROP_FILE =
<<~JAVASCRIPT
  (el, input) => {
    const dt = new DataTransfer();
    for (const file of input.files) { dt.items.add(file); }
    input.remove();
    el.dispatchEvent(new DragEvent('drop', {
      cancelable: true, bubbles: true, dataTransfer: dt
    }));
  }
JAVASCRIPT
DROP_STRING =
<<~JAVASCRIPT
  (el, items) => {
    const dt = new DataTransfer();
    for (const item of items) { dt.items.add(item.data, item.type); }
    el.dispatchEvent(new DragEvent('drop', {
      cancelable: true, bubbles: true, dataTransfer: dt
    }));
  }
JAVASCRIPT
SCROLL_POSITIONS =
{
  top: '0',
  bottom: 'el.scrollHeight',
  center: '(el.scrollHeight - el.clientHeight)/2'
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(driver, internal_logger, page, element) ⇒ Node

Returns a new instance of Node.



223
224
225
226
227
228
# File 'lib/capybara/playwright/node.rb', line 223

def initialize(driver, internal_logger, page, element)
  super(driver, element)
  @internal_logger = internal_logger
  @page = page
  @element = element
end

Instance Method Details

#==(other) ⇒ Object



1184
1185
1186
1187
1188
# File 'lib/capybara/playwright/node.rb', line 1184

def ==(other)
  return false unless other.is_a?(Node)

  @element.evaluate('(self, other) => self == other', arg: other.element)
end

#[](name) ⇒ Object



300
301
302
303
304
# File 'lib/capybara/playwright/node.rb', line 300

def [](name)
  assert_element_not_stale {
    property(name) || attribute(name)
  }
end

#all_textObject



267
268
269
270
271
272
273
274
275
276
# File 'lib/capybara/playwright/node.rb', line 267

def all_text
  assert_element_not_stale {
    text = @element.text_content
    text.to_s.gsub(/[\u200b\u200e\u200f]/, '')
        .gsub(/[\ \n\f\t\v\u2028\u2029]+/, ' ')
        .gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
        .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
        .tr("\u00a0", ' ')
  }
end

#checked?Boolean

Returns:

  • (Boolean)


1084
1085
1086
1087
1088
# File 'lib/capybara/playwright/node.rb', line 1084

def checked?
  assert_element_not_stale {
    @element.evaluate('el => !!el.checked')
  }
end

#click(keys = [], **options) ⇒ Object



572
573
574
575
# File 'lib/capybara/playwright/node.rb', line 572

def click(keys = [], **options)
  click_options = ClickOptions.new(@element, keys, options, capybara_default_wait_time)
  @element.click(**click_options.as_params)
end

#disabled?Boolean

Returns:

  • (Boolean)


1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
# File 'lib/capybara/playwright/node.rb', line 1096

def disabled?
  @element.evaluate(<<~JAVASCRIPT)
  function(el) {
    const xpath = 'parent::optgroup[@disabled] | \
                  ancestor::select[@disabled] | \
                  parent::fieldset[@disabled] | \
                  ancestor::*[not(self::legend) or preceding-sibling::legend][parent::fieldset[@disabled]]';
    return el.disabled || document.evaluate(xpath, el, null, XPathResult.BOOLEAN_TYPE, null).booleanValue
  }
  JAVASCRIPT
end

#double_click(keys = [], **options) ⇒ Object



584
585
586
587
# File 'lib/capybara/playwright/node.rb', line 584

def double_click(keys = [], **options)
  click_options = ClickOptions.new(@element, keys, options, capybara_default_wait_time)
  @element.dblclick(**click_options.as_params)
end

#drag_to(element, **options) ⇒ Object



846
847
848
# File 'lib/capybara/playwright/node.rb', line 846

def drag_to(element, **options)
  DragTo.new(@page, @element, element.element, options).execute
end

#drop(*args) ⇒ Object



952
953
954
955
956
957
958
959
960
961
# File 'lib/capybara/playwright/node.rb', line 952

def drop(*args)
  if args.first.is_a?(String) || args.first.is_a?(Pathname)
    input = @page.evaluate_handle(ATTACH_FILE)
    input.as_element.set_input_files(args.map(&:to_s))
    @element.evaluate(DROP_FILE, arg: input)
  else
    items = args.flat_map { |arg| arg.map { |(type, data)| { type: type, data: data } } }
    @element.evaluate(DROP_STRING, arg: items)
  end
end

#find_css(query, **options) ⇒ Object



1198
1199
1200
1201
1202
1203
1204
# File 'lib/capybara/playwright/node.rb', line 1198

def find_css(query, **options)
  assert_element_not_stale {
    @element.query_selector_all(query).map do |el|
      Node.new(@driver, @internal_logger, @page, el)
    end
  }
end

#find_xpath(query, **options) ⇒ Object



1190
1191
1192
1193
1194
1195
1196
# File 'lib/capybara/playwright/node.rb', line 1190

def find_xpath(query, **options)
  assert_element_not_stale {
    @element.query_selector_all("xpath=#{query}").map do |el|
      Node.new(@driver, @internal_logger, @page, el)
    end
  }
end

#hoverObject



842
843
844
# File 'lib/capybara/playwright/node.rb', line 842

def hover
  @element.hover(timeout: capybara_default_wait_time)
end

#inspectObject



1180
1181
1182
# File 'lib/capybara/playwright/node.rb', line 1180

def inspect
  %(#<#{self.class} tag="#{tag_name}" path="#{path}">)
end

#multiple?Boolean

Returns:

  • (Boolean)


1112
1113
1114
# File 'lib/capybara/playwright/node.rb', line 1112

def multiple?
  @element.evaluate('el => el.multiple')
end

#obscured?Boolean

Returns:

  • (Boolean)


1080
1081
1082
# File 'lib/capybara/playwright/node.rb', line 1080

def obscured?
  @element.capybara_obscured?
end

#pathObject



1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
# File 'lib/capybara/playwright/node.rb', line 1128

def path
  assert_element_not_stale {
    @element.evaluate(<<~JAVASCRIPT)
    (el) => {
      var xml = document;
      var xpath = '';
      var pos, tempitem2;
      if (el.getRootNode && el.getRootNode() instanceof ShadowRoot) {
        return "(: Shadow DOM element - no XPath :)";
      };
      while(el !== xml.documentElement) {
        pos = 0;
        tempitem2 = el;
        while(tempitem2) {
          if (tempitem2.nodeType === 1 && tempitem2.nodeName === el.nodeName) { // If it is ELEMENT_NODE of the same name
            pos += 1;
          }
          tempitem2 = tempitem2.previousSibling;
        }
        if (el.namespaceURI != xml.documentElement.namespaceURI) {
          xpath = "*[local-name()='"+el.nodeName+"' and namespace-uri()='"+(el.namespaceURI===null?'':el.namespaceURI)+"']["+pos+']'+'/'+xpath;
        } else {
          xpath = el.nodeName.toUpperCase()+"["+pos+"]/"+xpath;
        }
        el = el.parentNode;
        if (!el) {
          throw "(: Shadow DOM element - no XPath :)";
        }
      }
      xpath = '/'+xml.documentElement.nodeName.toUpperCase()+'/'+xpath;
      xpath = xpath.replace(/\\/$/, '');
      return xpath;
    }
    JAVASCRIPT
  }
end

#readonly?Boolean

Returns:

  • (Boolean)


1108
1109
1110
# File 'lib/capybara/playwright/node.rb', line 1108

def readonly?
  !@element.editable?
end

#rectObject



1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
# File 'lib/capybara/playwright/node.rb', line 1116

def rect
  assert_element_not_stale {
    @element.evaluate(<<~JAVASCRIPT)
    function(el){
      const rects = [...el.getClientRects()]
      const rect = rects.find(r => (r.height && r.width)) || el.getBoundingClientRect();
      return rect.toJSON();
    }
    JAVASCRIPT
  }
end

#right_click(keys = [], **options) ⇒ Object



577
578
579
580
581
582
# File 'lib/capybara/playwright/node.rb', line 577

def right_click(keys = [], **options)
  click_options = ClickOptions.new(@element, keys, options, capybara_default_wait_time)
  params = click_options.as_params
  params[:button] = 'right'
  @element.click(**params)
end

#scroll_by(x, y) ⇒ Object



963
964
965
966
967
968
969
970
971
972
973
974
975
976
# File 'lib/capybara/playwright/node.rb', line 963

def scroll_by(x, y)
  js = <<~JAVASCRIPT
  (el, [x, y]) => {
    if (el.scrollBy){
      el.scrollBy(x, y);
    } else {
      el.scrollTop = el.scrollTop + y;
      el.scrollLeft = el.scrollLeft + x;
    }
  }
  JAVASCRIPT

  @element.evaluate(js, arg: [x, y])
end

#scroll_to(element, location, position = nil) ⇒ Object



978
979
980
981
982
983
984
985
986
987
988
989
# File 'lib/capybara/playwright/node.rb', line 978

def scroll_to(element, location, position = nil)
  # location, element = element, nil if element.is_a? Symbol
  if element.is_a? Capybara::Playwright::Node
    scroll_element_to_location(element, location)
  elsif location.is_a? Symbol
    scroll_to_location(location)
  else
    scroll_to_coords(*position)
  end

  self
end

#select_optionObject



545
546
547
548
549
550
551
552
553
554
555
556
# File 'lib/capybara/playwright/node.rb', line 545

def select_option
  return false if disabled?

  select_element = parent_select_element
  if select_element.evaluate('el => el.multiple')
    selected_options = select_element.query_selector_all('option:checked')
    selected_options << @element
    select_element.select_option(element: selected_options, timeout: capybara_default_wait_time)
  else
    select_element.select_option(element: @element, timeout: capybara_default_wait_time)
  end
end

#selected?Boolean

Returns:

  • (Boolean)


1090
1091
1092
1093
1094
# File 'lib/capybara/playwright/node.rb', line 1090

def selected?
  assert_element_not_stale {
    @element.evaluate('el => !!el.selected')
  }
end

#send_keys(*args) ⇒ Object



668
669
670
# File 'lib/capybara/playwright/node.rb', line 668

def send_keys(*args)
  SendKeys.new(@element, args).execute
end

#set(value, **options) ⇒ Object

Parameters:

  • value (String, Array)

    Array is only allowed if node has 'multiple' attribute

  • options (Hash)

    Driver specific options for how to set a value on a node



337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
# File 'lib/capybara/playwright/node.rb', line 337

def set(value, **options)
  settable_class =
    case tag_name
    when 'input'
      case attribute('type')
      when 'radio'
        RadioButton
      when 'checkbox'
        Checkbox
      when 'file'
        FileUpload
      when 'date'
        DateInput
      when 'time'
        TimeInput
      when 'datetime-local'
        DateTimeInput
      when 'color'
        JSValueInput
      when 'range'
        JSValueInput
      else
        TextInput
      end
    when 'textarea'
      TextInput
    else
      if @element.editable?
        TextInput
      else
        raise NotSupportedByDriverError
      end
    end

  settable_class.new(@element, capybara_default_wait_time, @internal_logger).set(value, **options)
rescue ::Playwright::TimeoutError => err
  raise NotActionableError.new(err)
end

#shadow_rootObject



1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
# File 'lib/capybara/playwright/node.rb', line 1169

def shadow_root
  # Playwright does not distinguish shadow DOM.
  # https://playwright.dev/docs/selectors#selecting-elements-in-shadow-dom
  # Just do with Host element as shadow root Element.
  #
  #   Node.new(@driver, @page, @element.evaluate_handle('el => el.shadowRoot'))
  #
  # does not work well because of the Playwright Error 'Element is not attached to the DOM'
  ShadowRootNode.new(@driver, @internal_logger, @page, @element)
end

#style(styles) ⇒ Object

Raises:

  • (NotImplementedError)


329
330
331
332
333
# File 'lib/capybara/playwright/node.rb', line 329

def style(styles)
  # Capybara provides default implementation.
  # ref: https://github.com/teamcapybara/capybara/blob/f7ab0b5cd5da86185816c2d5c30d58145fe654ed/lib/capybara/node/element.rb#L92
  raise NotImplementedError
end

#tag_nameObject



1042
1043
1044
# File 'lib/capybara/playwright/node.rb', line 1042

def tag_name
  @tag_name ||= @element.evaluate('e => e.tagName.toLowerCase()')
end

#trigger(event) ⇒ Object



1165
1166
1167
# File 'lib/capybara/playwright/node.rb', line 1165

def trigger(event)
  @element.dispatch_event(event)
end

#unselect_optionObject



558
559
560
561
562
563
564
565
566
# File 'lib/capybara/playwright/node.rb', line 558

def unselect_option
  if parent_select_element.evaluate('el => el.multiple')
    return false if disabled?

    @element.evaluate('el => el.selected = false')
  else
    raise Capybara::UnselectNotAllowed, 'Cannot unselect option from single select box.'
  end
end

#valueObject



315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/capybara/playwright/node.rb', line 315

def value
  assert_element_not_stale {
    # ref: https://github.com/teamcapybara/capybara/blob/f7ab0b5cd5da86185816c2d5c30d58145fe654ed/lib/capybara/selenium/node.rb#L31
    # ref: https://github.com/twalpole/apparition/blob/11aca464b38b77585191b7e302be2e062bdd369d/lib/capybara/apparition/node.rb#L728
    if tag_name == 'select' && @element.evaluate('el => el.multiple')
      @element.query_selector_all('option:checked').map do |option|
        option.evaluate('el => el.value')
      end
    else
      @element.evaluate('el => el.value')
    end
  }
end

#visible?Boolean

Returns:

  • (Boolean)


1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
# File 'lib/capybara/playwright/node.rb', line 1046

def visible?
  assert_element_not_stale {
    # if an area element, check visibility of relevant image
    @element.evaluate(<<~JAVASCRIPT)
    function(el) {
      if (el.tagName == 'AREA'){
        const map_name = document.evaluate('./ancestor::map/@name', el, null, XPathResult.STRING_TYPE, null).stringValue;
        el = document.querySelector(`img[usemap='#${map_name}']`);
        if (!el){
        return false;
        }
      }
      var forced_visible = false;
      while (el) {
        const style = window.getComputedStyle(el);
        if (style.visibility == 'visible')
          forced_visible = true;
        if ((style.display == 'none') ||
            ((style.visibility == 'hidden') && !forced_visible) ||
            (parseFloat(style.opacity) == 0)) {
          return false;
        }
        var parent = el.parentElement;
        if (parent && (parent.tagName == 'DETAILS') && !parent.open && (el.tagName != 'SUMMARY')) {
          return false;
        }
        el = parent;
      }
      return true;
    }
    JAVASCRIPT
  }
end

#visible_textObject



278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
# File 'lib/capybara/playwright/node.rb', line 278

def visible_text
  assert_element_not_stale {
    return '' unless visible?

    text = @element.evaluate(<<~JAVASCRIPT)
      function(el){
        if (el.nodeName == 'TEXTAREA'){
          return el.textContent;
        } else if (el instanceof SVGElement) {
          return el.textContent;
        } else {
          return el.innerText;
        }
      }
    JAVASCRIPT
    text.to_s.scrub.gsub(/\A[[:space:]&&[^\u00a0]]+/, '')
        .gsub(/[[:space:]&&[^\u00a0]]+\z/, '')
        .gsub(/\n+/, "\n")
        .tr("\u00a0", ' ')
  }
end