<commit_before>/*
 * Copyright 2005 Shinya Kasatani
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//import { MozillaBrowserBot } from "./selenium-browserbot";
import { bot, core } from './closure-polyfill'
import { parse_locator } from './utils'
import finder from '@medv/finder'

export default function LocatorBuilders(window) {
  this.window = window
}

window.LocatorBuilders = LocatorBuilders

LocatorBuilders.prototype.detach = function() {}

LocatorBuilders.prototype.buildWith = function(name, e, opt_contextNode) {
  return LocatorBuilders.builderMap[name].call(this, e, opt_contextNode)
}

LocatorBuilders.prototype.elementEquals = function(name, e, locator) {
  let fe = this.findElement(locator)
  //TODO: add match function to the ui locator builder, note the inverted parameters
  return (
    e == fe ||
    (LocatorBuilders.builderMap[name] &&
      LocatorBuilders.builderMap[name].match &&
      LocatorBuilders.builderMap[name].match(e, fe))
  )
}

LocatorBuilders.prototype.build = function(e) {
  let locators = this.buildAll(e)
  if (locators.length > 0) {
    return locators[0][0]
  } else {
    return 'LOCATOR_DETECTION_FAILED'
  }
}

LocatorBuilders.prototype.buildAll = function(el) {
  let e = core.firefox.unwrap(el) //Samit: Fix: Do the magic to get it to work in Firefox 4
  let locator
  let locators = []
  for (let i = 0; i < LocatorBuilders.order.length; i++) {
    let finderName = LocatorBuilders.order[i]
    try {
      locator = this.buildWith(finderName, e)
      if (locator) {
        locator = String(locator)
        //Samit: The following is a quickfix for above commented code to stop exceptions on almost every locator builder
        //TODO: the builderName should NOT be used as a strategy name, create a feature to allow locatorBuilders to specify this kind of behaviour
        //TODO: Useful if a builder wants to capture a different element like a parent. Use the this.elementEquals
        let fe = this.findElement(locator)
        if (e == fe) {
          locators.push([locator, finderName])
        }
      }
    } catch (e) {
      // TODO ignore the buggy locator builder for now
      //this.log.debug("locator exception: " + e);
    }
  }
  return locators
}

LocatorBuilders.prototype.findElement = function(loc) {
  try {
    const locator = parse_locator(loc, true)
    return bot.locators.findElement(
      { [locator.type]: locator.string },
      this.window.document
    )
  } catch (error) {
    //this.log.debug("findElement failed: " + error + ", locator=" + locator);
    return null
  }
}

/*
 * Class methods
 */

LocatorBuilders.order = []
LocatorBuilders.builderMap = {}
LocatorBuilders._preferredOrder = []
// NOTE: for some reasons we does not use this part
// classObservable(LocatorBuilders);

LocatorBuilders.add = function(name, finder) {
  this.order.push(name)
  this.builderMap[name] = finder
  this._orderChanged()
}

/**
 * Call when the order or preferred order changes
 */
LocatorBuilders._orderChanged = function() {
  let changed = this._ensureAllPresent(this.order, this._preferredOrder)
  this._sortByRefOrder(this.order, this._preferredOrder)
  if (changed) {
    // NOTE: for some reasons we does not use this part
    // this.notify('preferredOrderChanged', this._preferredOrder);
  }
}

/**
 * Set the preferred order of the locator builders
 *
 * @param preferredOrder can be an array or a comma separated string of names
 */
LocatorBuilders.setPreferredOrder = function(preferredOrder) {
  if (typeof preferredOrder === 'string') {
    this._preferredOrder = preferredOrder.split(',')
  } else {
    this._preferredOrder = preferredOrder
  }
  this._orderChanged()
}

/**
 * Returns the locator builders preferred order as an array
 */
LocatorBuilders.getPreferredOrder = function() {
  return this._preferredOrder
}

/**
 * Sorts arrayToSort in the order of elements in sortOrderReference
 * @param arrayToSort
 * @param sortOrderReference
 */
LocatorBuilders._sortByRefOrder = function(arrayToSort, sortOrderReference) {
  let raLen = sortOrderReference.length
  arrayToSort.sort(function(a, b) {
    let ai = sortOrderReference.indexOf(a)
    let bi = sortOrderReference.indexOf(b)
    return (ai > -1 ? ai : raLen) - (bi > -1 ? bi : raLen)
  })
}

/**
 * Function to add to the bottom of destArray elements from source array that do not exist in destArray
 * @param sourceArray
 * @param destArray
 */
LocatorBuilders._ensureAllPresent = function(sourceArray, destArray) {
  let changed = false
  sourceArray.forEach(function(e) {
    if (destArray.indexOf(e) == -1) {
      destArray.push(e)
      changed = true
    }
  })
  return changed
}

/*
 * Utility function: Encode XPath attribute value.
 */
LocatorBuilders.prototype.attributeValue = function(value) {
  if (value.indexOf("'") < 0) {
    return "'" + value + "'"
  } else if (value.indexOf('"') < 0) {
    return '"' + value + '"'
  } else {
    let result = 'concat('
    let part = ''
    let didReachEndOfValue = false
    while (!didReachEndOfValue) {
      let apos = value.indexOf("'")
      let quot = value.indexOf('"')
      if (apos < 0) {
        result += "'" + value + "'"
        didReachEndOfValue = true
        break
      } else if (quot < 0) {
        result += '"' + value + '"'
        didReachEndOfValue = true
        break
      } else if (quot < apos) {
        part = value.substring(0, apos)
        result += "'" + part + "'"
        value = value.substring(part.length)
      } else {
        part = value.substring(0, quot)
        result += '"' + part + '"'
        value = value.substring(part.length)
      }
      result += ','
    }
    result += ')'
    return result
  }
}

LocatorBuilders.prototype.xpathHtmlElement = function(name) {
  if (this.window.document.contentType == 'application/xhtml+xml') {
    // "x:" prefix is required when testing XHTML pages
    return 'x:' + name
  } else {
    return name
  }
}

LocatorBuilders.prototype.relativeXPathFromParent = function(current) {
  let index = this.getNodeNbr(current)
  let currentPath = '/' + this.xpathHtmlElement(current.nodeName.toLowerCase())
  if (index > 0) {
    currentPath += '[' + (index + 1) + ']'
  }
  return currentPath
}

LocatorBuilders.prototype.getNodeNbr = function(current) {
  let childNodes = current.parentNode.childNodes
  let total = 0
  let index = -1
  for (let i = 0; i < childNodes.length; i++) {
    let child = childNodes[i]
    if (child.nodeName == current.nodeName) {
      if (child == current) {
        index = total
      }
      total++
    }
  }
  return index
}

LocatorBuilders.prototype.preciseXPath = function(xpath, e) {
  //only create more precise xpath if needed
  if (this.findElement(xpath) != e) {
    let result = e.ownerDocument.evaluate(
      xpath,
      e.ownerDocument,
      null,
      XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
      null
    )
    //skip first element (result:0 xpath index:1)
    for (let i = 0, len = result.snapshotLength; i < len; i++) {
      let newPath = 'xpath=(' + xpath + ')[' + (i + 1) + ']'
      if (this.findElement(newPath) == e) {
        return newPath
      }
    }
  }
  return 'xpath=' + xpath
}

/*
 * ===== builders =====
 */

// order listed dictates priority
// e.g., 1st listed is top priority

LocatorBuilders.add('css:data-attr', function(e) {
  const dataAttributes = ['data-test', 'data-test-id']
  for (let i = 0; i < dataAttributes.length; i++) {
    const attr = dataAttributes[i]
    const value = e.getAttribute(attr)
    if (attr) {
      return `css=*[${attr}="${value}"]`
    }
  }
  return null
})

LocatorBuilders.add('id', function(e) {
  if (e.id) {
    return 'id=' + e.id
  }
  return null
})

LocatorBuilders.add('linkText', function(e) {
  if (e.nodeName == 'A') {
    let text = e.textContent
    if (!text.match(/^\s*$/)) {
      return (
        'linkText=' + text.replace(/\xA0/g, ' ').replace(/^\s*(.*?)\s*$/, '$1')
      )
    }
  }
  return null
})

LocatorBuilders.add('name', function(e) {
  if (e.name) {
    return 'name=' + e.name
  }
  return null
})

LocatorBuilders.add('css:finder', function(e) {
  return 'css=' + finder(e)
})

LocatorBuilders.add('xpath:link', function(e) {
  if (e.nodeName == 'A') {
    let text = e.textContent
    if (!text.match(/^\s*$/)) {
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('a') +
          "[contains(text(),'" +
          text.replace(/^\s+/, '').replace(/\s+$/, '') +
          "')]",
        e
      )
    }
  }
  return null
})

LocatorBuilders.add('xpath:img', function(e) {
  if (e.nodeName == 'IMG') {
    if (e.alt != '') {
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('img') +
          '[@alt=' +
          this.attributeValue(e.alt) +
          ']',
        e
      )
    } else if (e.title != '') {
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('img') +
          '[@title=' +
          this.attributeValue(e.title) +
          ']',
        e
      )
    } else if (e.src != '') {
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('img') +
          '[contains(@src,' +
          this.attributeValue(e.src) +
          ')]',
        e
      )
    }
  }
  return null
})

LocatorBuilders.add('xpath:attributes', function(e) {
  const PREFERRED_ATTRIBUTES = [
    'id',
    'name',
    'value',
    'type',
    'action',
    'onclick',
  ]
  let i = 0

  function attributesXPath(name, attNames, attributes) {
    let locator = '//' + this.xpathHtmlElement(name) + '['
    for (i = 0; i < attNames.length; i++) {
      if (i > 0) {
        locator += ' and '
      }
      let attName = attNames[i]
      locator += '@' + attName + '=' + this.attributeValue(attributes[attName])
    }
    locator += ']'
    return this.preciseXPath(locator, e)
  }

  if (e.attributes) {
    let atts = e.attributes
    let attsMap = {}
    for (i = 0; i < atts.length; i++) {
      let att = atts[i]
      attsMap[att.name] = att.value
    }
    let names = []
    // try preferred attributes
    for (i = 0; i < PREFERRED_ATTRIBUTES.length; i++) {
      let name = PREFERRED_ATTRIBUTES[i]
      if (attsMap[name] != null) {
        names.push(name)
        let locator = attributesXPath.call(
          this,
          e.nodeName.toLowerCase(),
          names,
          attsMap
        )
        if (e == this.findElement(locator)) {
          return locator
        }
      }
    }
  }
  return null
})

LocatorBuilders.add('xpath:idRelative', function(e) {
  let path = ''
  let current = e
  while (current != null) {
    if (current.parentNode != null) {
      path = this.relativeXPathFromParent(current) + path
      if (
        1 == current.parentNode.nodeType && // ELEMENT_NODE
        current.parentNode.getAttribute('id')
      ) {
        return this.preciseXPath(
          '//' +
            this.xpathHtmlElement(current.parentNode.nodeName.toLowerCase()) +
            '[@id=' +
            this.attributeValue(current.parentNode.getAttribute('id')) +
            ']' +
            path,
          e
        )
      }
    } else {
      return null
    }
    current = current.parentNode
  }
  return null
})

LocatorBuilders.add('xpath:href', function(e) {
  if (e.attributes && e.hasAttribute('href')) {
    let href = e.getAttribute('href')
    if (href.search(/^http?:\/\//) >= 0) {
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('a') +
          '[@href=' +
          this.attributeValue(href) +
          ']',
        e
      )
    } else {
      // use contains(), because in IE getAttribute("href") will return absolute path
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('a') +
          '[contains(@href, ' +
          this.attributeValue(href) +
          ')]',
        e
      )
    }
  }
  return null
})

LocatorBuilders.add('xpath:position', function(e, opt_contextNode) {
  let path = ''
  let current = e
  while (current != null && current != opt_contextNode) {
    let currentPath
    if (current.parentNode != null) {
      currentPath = this.relativeXPathFromParent(current)
    } else {
      currentPath = '/' + this.xpathHtmlElement(current.nodeName.toLowerCase())
    }
    path = currentPath + path
    let locator = '/' + path
    if (e == this.findElement(locator)) {
      return 'xpath=' + locator
    }
    current = current.parentNode
  }
  return null
})

//LocatorBuilders.add('xpath:innerText', function(el) {
//  if (el.innerText && el.nodeName !== 'DIV') {
//    const initialXpath = `//${el.nodeName.toLowerCase()}[contains(.,'${
//      el.innerText
//    }')]`
//    return this.preciseXPath(initialXpath, el)
//  } else {
//    return null
//  }
//})
<commit_msg>Re-added the xpath:innerText locator strategy, without the check against preciseXPath<commit_after>/*
 * Copyright 2005 Shinya Kasatani
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//import { MozillaBrowserBot } from "./selenium-browserbot";
import { bot, core } from './closure-polyfill'
import { parse_locator } from './utils'
import finder from '@medv/finder'

export default function LocatorBuilders(window) {
  this.window = window
}

window.LocatorBuilders = LocatorBuilders

LocatorBuilders.prototype.detach = function() {}

LocatorBuilders.prototype.buildWith = function(name, e, opt_contextNode) {
  return LocatorBuilders.builderMap[name].call(this, e, opt_contextNode)
}

LocatorBuilders.prototype.elementEquals = function(name, e, locator) {
  let fe = this.findElement(locator)
  //TODO: add match function to the ui locator builder, note the inverted parameters
  return (
    e == fe ||
    (LocatorBuilders.builderMap[name] &&
      LocatorBuilders.builderMap[name].match &&
      LocatorBuilders.builderMap[name].match(e, fe))
  )
}

LocatorBuilders.prototype.build = function(e) {
  let locators = this.buildAll(e)
  if (locators.length > 0) {
    return locators[0][0]
  } else {
    return 'LOCATOR_DETECTION_FAILED'
  }
}

LocatorBuilders.prototype.buildAll = function(el) {
  let e = core.firefox.unwrap(el) //Samit: Fix: Do the magic to get it to work in Firefox 4
  let locator
  let locators = []
  for (let i = 0; i < LocatorBuilders.order.length; i++) {
    let finderName = LocatorBuilders.order[i]
    try {
      locator = this.buildWith(finderName, e)
      if (locator) {
        locator = String(locator)
        //Samit: The following is a quickfix for above commented code to stop exceptions on almost every locator builder
        //TODO: the builderName should NOT be used as a strategy name, create a feature to allow locatorBuilders to specify this kind of behaviour
        //TODO: Useful if a builder wants to capture a different element like a parent. Use the this.elementEquals
        let fe = this.findElement(locator)
        if (e == fe) {
          locators.push([locator, finderName])
        }
      }
    } catch (e) {
      // TODO ignore the buggy locator builder for now
      //this.log.debug("locator exception: " + e);
    }
  }
  return locators
}

LocatorBuilders.prototype.findElement = function(loc) {
  try {
    const locator = parse_locator(loc, true)
    return bot.locators.findElement(
      { [locator.type]: locator.string },
      this.window.document
    )
  } catch (error) {
    //this.log.debug("findElement failed: " + error + ", locator=" + locator);
    return null
  }
}

/*
 * Class methods
 */

LocatorBuilders.order = []
LocatorBuilders.builderMap = {}
LocatorBuilders._preferredOrder = []
// NOTE: for some reasons we does not use this part
// classObservable(LocatorBuilders);

LocatorBuilders.add = function(name, finder) {
  this.order.push(name)
  this.builderMap[name] = finder
  this._orderChanged()
}

/**
 * Call when the order or preferred order changes
 */
LocatorBuilders._orderChanged = function() {
  let changed = this._ensureAllPresent(this.order, this._preferredOrder)
  this._sortByRefOrder(this.order, this._preferredOrder)
  if (changed) {
    // NOTE: for some reasons we does not use this part
    // this.notify('preferredOrderChanged', this._preferredOrder);
  }
}

/**
 * Set the preferred order of the locator builders
 *
 * @param preferredOrder can be an array or a comma separated string of names
 */
LocatorBuilders.setPreferredOrder = function(preferredOrder) {
  if (typeof preferredOrder === 'string') {
    this._preferredOrder = preferredOrder.split(',')
  } else {
    this._preferredOrder = preferredOrder
  }
  this._orderChanged()
}

/**
 * Returns the locator builders preferred order as an array
 */
LocatorBuilders.getPreferredOrder = function() {
  return this._preferredOrder
}

/**
 * Sorts arrayToSort in the order of elements in sortOrderReference
 * @param arrayToSort
 * @param sortOrderReference
 */
LocatorBuilders._sortByRefOrder = function(arrayToSort, sortOrderReference) {
  let raLen = sortOrderReference.length
  arrayToSort.sort(function(a, b) {
    let ai = sortOrderReference.indexOf(a)
    let bi = sortOrderReference.indexOf(b)
    return (ai > -1 ? ai : raLen) - (bi > -1 ? bi : raLen)
  })
}

/**
 * Function to add to the bottom of destArray elements from source array that do not exist in destArray
 * @param sourceArray
 * @param destArray
 */
LocatorBuilders._ensureAllPresent = function(sourceArray, destArray) {
  let changed = false
  sourceArray.forEach(function(e) {
    if (destArray.indexOf(e) == -1) {
      destArray.push(e)
      changed = true
    }
  })
  return changed
}

/*
 * Utility function: Encode XPath attribute value.
 */
LocatorBuilders.prototype.attributeValue = function(value) {
  if (value.indexOf("'") < 0) {
    return "'" + value + "'"
  } else if (value.indexOf('"') < 0) {
    return '"' + value + '"'
  } else {
    let result = 'concat('
    let part = ''
    let didReachEndOfValue = false
    while (!didReachEndOfValue) {
      let apos = value.indexOf("'")
      let quot = value.indexOf('"')
      if (apos < 0) {
        result += "'" + value + "'"
        didReachEndOfValue = true
        break
      } else if (quot < 0) {
        result += '"' + value + '"'
        didReachEndOfValue = true
        break
      } else if (quot < apos) {
        part = value.substring(0, apos)
        result += "'" + part + "'"
        value = value.substring(part.length)
      } else {
        part = value.substring(0, quot)
        result += '"' + part + '"'
        value = value.substring(part.length)
      }
      result += ','
    }
    result += ')'
    return result
  }
}

LocatorBuilders.prototype.xpathHtmlElement = function(name) {
  if (this.window.document.contentType == 'application/xhtml+xml') {
    // "x:" prefix is required when testing XHTML pages
    return 'x:' + name
  } else {
    return name
  }
}

LocatorBuilders.prototype.relativeXPathFromParent = function(current) {
  let index = this.getNodeNbr(current)
  let currentPath = '/' + this.xpathHtmlElement(current.nodeName.toLowerCase())
  if (index > 0) {
    currentPath += '[' + (index + 1) + ']'
  }
  return currentPath
}

LocatorBuilders.prototype.getNodeNbr = function(current) {
  let childNodes = current.parentNode.childNodes
  let total = 0
  let index = -1
  for (let i = 0; i < childNodes.length; i++) {
    let child = childNodes[i]
    if (child.nodeName == current.nodeName) {
      if (child == current) {
        index = total
      }
      total++
    }
  }
  return index
}

LocatorBuilders.prototype.preciseXPath = function(xpath, e) {
  //only create more precise xpath if needed
  if (this.findElement(xpath) != e) {
    let result = e.ownerDocument.evaluate(
      xpath,
      e.ownerDocument,
      null,
      XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
      null
    )
    //skip first element (result:0 xpath index:1)
    for (let i = 0, len = result.snapshotLength; i < len; i++) {
      let newPath = 'xpath=(' + xpath + ')[' + (i + 1) + ']'
      if (this.findElement(newPath) == e) {
        return newPath
      }
    }
  }
  return 'xpath=' + xpath
}

/*
 * ===== builders =====
 */

// order listed dictates priority
// e.g., 1st listed is top priority

LocatorBuilders.add('css:data-attr', function(e) {
  const dataAttributes = ['data-test', 'data-test-id']
  for (let i = 0; i < dataAttributes.length; i++) {
    const attr = dataAttributes[i]
    const value = e.getAttribute(attr)
    if (attr) {
      return `css=*[${attr}="${value}"]`
    }
  }
  return null
})

LocatorBuilders.add('id', function(e) {
  if (e.id) {
    return 'id=' + e.id
  }
  return null
})

LocatorBuilders.add('linkText', function(e) {
  if (e.nodeName == 'A') {
    let text = e.textContent
    if (!text.match(/^\s*$/)) {
      return (
        'linkText=' + text.replace(/\xA0/g, ' ').replace(/^\s*(.*?)\s*$/, '$1')
      )
    }
  }
  return null
})

LocatorBuilders.add('name', function(e) {
  if (e.name) {
    return 'name=' + e.name
  }
  return null
})

LocatorBuilders.add('css:finder', function(e) {
  return 'css=' + finder(e)
})

LocatorBuilders.add('xpath:link', function(e) {
  if (e.nodeName == 'A') {
    let text = e.textContent
    if (!text.match(/^\s*$/)) {
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('a') +
          "[contains(text(),'" +
          text.replace(/^\s+/, '').replace(/\s+$/, '') +
          "')]",
        e
      )
    }
  }
  return null
})

LocatorBuilders.add('xpath:img', function(e) {
  if (e.nodeName == 'IMG') {
    if (e.alt != '') {
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('img') +
          '[@alt=' +
          this.attributeValue(e.alt) +
          ']',
        e
      )
    } else if (e.title != '') {
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('img') +
          '[@title=' +
          this.attributeValue(e.title) +
          ']',
        e
      )
    } else if (e.src != '') {
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('img') +
          '[contains(@src,' +
          this.attributeValue(e.src) +
          ')]',
        e
      )
    }
  }
  return null
})

LocatorBuilders.add('xpath:attributes', function(e) {
  const PREFERRED_ATTRIBUTES = [
    'id',
    'name',
    'value',
    'type',
    'action',
    'onclick',
  ]
  let i = 0

  function attributesXPath(name, attNames, attributes) {
    let locator = '//' + this.xpathHtmlElement(name) + '['
    for (i = 0; i < attNames.length; i++) {
      if (i > 0) {
        locator += ' and '
      }
      let attName = attNames[i]
      locator += '@' + attName + '=' + this.attributeValue(attributes[attName])
    }
    locator += ']'
    return this.preciseXPath(locator, e)
  }

  if (e.attributes) {
    let atts = e.attributes
    let attsMap = {}
    for (i = 0; i < atts.length; i++) {
      let att = atts[i]
      attsMap[att.name] = att.value
    }
    let names = []
    // try preferred attributes
    for (i = 0; i < PREFERRED_ATTRIBUTES.length; i++) {
      let name = PREFERRED_ATTRIBUTES[i]
      if (attsMap[name] != null) {
        names.push(name)
        let locator = attributesXPath.call(
          this,
          e.nodeName.toLowerCase(),
          names,
          attsMap
        )
        if (e == this.findElement(locator)) {
          return locator
        }
      }
    }
  }
  return null
})

LocatorBuilders.add('xpath:idRelative', function(e) {
  let path = ''
  let current = e
  while (current != null) {
    if (current.parentNode != null) {
      path = this.relativeXPathFromParent(current) + path
      if (
        1 == current.parentNode.nodeType && // ELEMENT_NODE
        current.parentNode.getAttribute('id')
      ) {
        return this.preciseXPath(
          '//' +
            this.xpathHtmlElement(current.parentNode.nodeName.toLowerCase()) +
            '[@id=' +
            this.attributeValue(current.parentNode.getAttribute('id')) +
            ']' +
            path,
          e
        )
      }
    } else {
      return null
    }
    current = current.parentNode
  }
  return null
})

LocatorBuilders.add('xpath:href', function(e) {
  if (e.attributes && e.hasAttribute('href')) {
    let href = e.getAttribute('href')
    if (href.search(/^http?:\/\//) >= 0) {
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('a') +
          '[@href=' +
          this.attributeValue(href) +
          ']',
        e
      )
    } else {
      // use contains(), because in IE getAttribute("href") will return absolute path
      return this.preciseXPath(
        '//' +
          this.xpathHtmlElement('a') +
          '[contains(@href, ' +
          this.attributeValue(href) +
          ')]',
        e
      )
    }
  }
  return null
})

LocatorBuilders.add('xpath:position', function(e, opt_contextNode) {
  let path = ''
  let current = e
  while (current != null && current != opt_contextNode) {
    let currentPath
    if (current.parentNode != null) {
      currentPath = this.relativeXPathFromParent(current)
    } else {
      currentPath = '/' + this.xpathHtmlElement(current.nodeName.toLowerCase())
    }
    path = currentPath + path
    let locator = '/' + path
    if (e == this.findElement(locator)) {
      return 'xpath=' + locator
    }
    current = current.parentNode
  }
  return null
})

LocatorBuilders.add('xpath:innerText', function(el) {
  if (el.innerText) {
    return `xpath=//${el.nodeName.toLowerCase()}[contains(.,'${el.innerText}')]`
  } else {
    return null
  }
})
<|endoftext|>


<commit_before>/*
 * Copyright 2005 Shinya Kasatani
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//import { MozillaBrowserBot } from "./selenium-browserbot";
import { bot, core } from "./closure-polyfill";
import { parse_locator } from "./utils";
import finder from "@medv/finder";

export default function LocatorBuilders(window) {
  this.window = window;
}

window.LocatorBuilders = LocatorBuilders;

LocatorBuilders.prototype.detach = function() {
};

LocatorBuilders.prototype.buildWith = function(name, e, opt_contextNode) {
  return LocatorBuilders.builderMap[name].call(this, e, opt_contextNode);
};

LocatorBuilders.prototype.elementEquals = function(name, e, locator) {
  let fe = this.findElement(locator);
  //TODO: add match function to the ui locator builder, note the inverted parameters
  return (e == fe) || (LocatorBuilders.builderMap[name] && LocatorBuilders.builderMap[name].match && LocatorBuilders.builderMap[name].match(e, fe));
};

LocatorBuilders.prototype.build = function(e) {
  let locators = this.buildAll(e);
  if (locators.length > 0) {
    return locators[0][0];
  } else {
    return "LOCATOR_DETECTION_FAILED";
  }
};

LocatorBuilders.prototype.buildAll = function(el) {
  let e = core.firefox.unwrap(el); //Samit: Fix: Do the magic to get it to work in Firefox 4
  let locator;
  let locators = [];
  for (let i = 0; i < LocatorBuilders.order.length; i++) {
    let finderName = LocatorBuilders.order[i];
    try {
      locator = this.buildWith(finderName, e);
      if (locator) {
        locator = String(locator);
        //Samit: The following is a quickfix for above commented code to stop exceptions on almost every locator builder
        //TODO: the builderName should NOT be used as a strategy name, create a feature to allow locatorBuilders to specify this kind of behaviour
        //TODO: Useful if a builder wants to capture a different element like a parent. Use the this.elementEquals
        let fe = this.findElement(locator);
        if (e == fe) {
          locators.push([locator, finderName]);
        }
      }
    } catch (e) {
      // TODO ignore the buggy locator builder for now
      //this.log.debug("locator exception: " + e);
    }
  }
  return locators;
};

LocatorBuilders.prototype.findElement = function(loc) {
  try {
    const locator = parse_locator(loc, true);
    return bot.locators.findElement({ [locator.type]: locator.string }, this.window.document);
  } catch (error) {
    //this.log.debug("findElement failed: " + error + ", locator=" + locator);
    return null;
  }
};

/*
 * Class methods
 */

LocatorBuilders.order = [];
LocatorBuilders.builderMap = {};
LocatorBuilders._preferredOrder = [];
// NOTE: for some reasons we does not use this part
// classObservable(LocatorBuilders);

LocatorBuilders.add = function(name, finder) {
  this.order.push(name);
  this.builderMap[name] = finder;
  this._orderChanged();
};

/**
 * Call when the order or preferred order changes
 */
LocatorBuilders._orderChanged = function() {
  let changed = this._ensureAllPresent(this.order, this._preferredOrder);
  this._sortByRefOrder(this.order, this._preferredOrder);
  if (changed) {
    // NOTE: for some reasons we does not use this part
    // this.notify('preferredOrderChanged', this._preferredOrder);
  }
};

/**
 * Set the preferred order of the locator builders
 *
 * @param preferredOrder can be an array or a comma separated string of names
 */
LocatorBuilders.setPreferredOrder = function(preferredOrder) {
  if (typeof preferredOrder === "string") {
    this._preferredOrder = preferredOrder.split(",");
  } else {
    this._preferredOrder = preferredOrder;
  }
  this._orderChanged();
};

/**
 * Returns the locator builders preferred order as an array
 */
LocatorBuilders.getPreferredOrder = function() {
  return this._preferredOrder;
};

/**
 * Sorts arrayToSort in the order of elements in sortOrderReference
 * @param arrayToSort
 * @param sortOrderReference
 */
LocatorBuilders._sortByRefOrder = function(arrayToSort, sortOrderReference) {
  let raLen = sortOrderReference.length;
  arrayToSort.sort(function(a, b) {
    let ai = sortOrderReference.indexOf(a);
    let bi = sortOrderReference.indexOf(b);
    return (ai > -1 ? ai : raLen) - (bi > -1 ? bi : raLen);
  });
};

/**
 * Function to add to the bottom of destArray elements from source array that do not exist in destArray
 * @param sourceArray
 * @param destArray
 */
LocatorBuilders._ensureAllPresent = function(sourceArray, destArray) {
  let changed = false;
  sourceArray.forEach(function(e) {
    if (destArray.indexOf(e) == -1) {
      destArray.push(e);
      changed = true;
    }
  });
  return changed;
};

/*
 * Utility function: Encode XPath attribute value.
 */
LocatorBuilders.prototype.attributeValue = function(value) {
  if (value.indexOf("'") < 0) {
    return "'" + value + "'";
  } else if (value.indexOf("\"") < 0) {
    return "\"" + value + "\"";
  } else {
    let result = "concat(";
    let part = "";
    let didReachEndOfValue = false;
    while (!didReachEndOfValue) {
      let apos = value.indexOf("'");
      let quot = value.indexOf("\"");
      if (apos < 0) {
        result += "'" + value + "'";
        didReachEndOfValue = true;
        break;
      } else if (quot < 0) {
        result += "\"" + value + "\"";
        didReachEndOfValue = true;
        break;
      } else if (quot < apos) {
        part = value.substring(0, apos);
        result += "'" + part + "'";
        value = value.substring(part.length);
      } else {
        part = value.substring(0, quot);
        result += "\"" + part + "\"";
        value = value.substring(part.length);
      }
      result += ",";
    }
    result += ")";
    return result;
  }
};

LocatorBuilders.prototype.xpathHtmlElement = function(name) {
  if (this.window.document.contentType == "application/xhtml+xml") {
    // "x:" prefix is required when testing XHTML pages
    return "x:" + name;
  } else {
    return name;
  }
};

LocatorBuilders.prototype.relativeXPathFromParent = function(current) {
  let index = this.getNodeNbr(current);
  let currentPath = "/" + this.xpathHtmlElement(current.nodeName.toLowerCase());
  if (index > 0) {
    currentPath += "[" + (index + 1) + "]";
  }
  return currentPath;
};

LocatorBuilders.prototype.getNodeNbr = function(current) {
  let childNodes = current.parentNode.childNodes;
  let total = 0;
  let index = -1;
  for (let i = 0; i < childNodes.length; i++) {
    let child = childNodes[i];
    if (child.nodeName == current.nodeName) {
      if (child == current) {
        index = total;
      }
      total++;
    }
  }
  return index;
};

LocatorBuilders.prototype.getCSSSubPath = function(e) {
  let css_attributes = ["id", "name", "class", "type", "alt", "title", "value"];
  for (let i = 0; i < css_attributes.length; i++) {
    let attr = css_attributes[i];
    let value = e.getAttribute(attr);
    if (value) {
      if (attr == "id")
        return "#" + value;
      if (attr == "class")
        return e.nodeName.toLowerCase() + "." + value.replace(" ", ".").replace("..", ".");
      return e.nodeName.toLowerCase() + "[" + attr + "=\"" + value + "\"]";
    }
  }
  if (this.getNodeNbr(e))
    return e.nodeName.toLowerCase() + ":nth-of-type(" + this.getNodeNbr(e) + ")";
  else
    return e.nodeName.toLowerCase();
};

LocatorBuilders.prototype.preciseXPath = function(xpath, e) {
  //only create more precise xpath if needed
  if (this.findElement(xpath) != e) {
    let result = e.ownerDocument.evaluate(xpath, e.ownerDocument, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    //skip first element (result:0 xpath index:1)
    for (let i = 0, len = result.snapshotLength; i < len; i++) {
      let newPath = "xpath=(" + xpath + ")[" + (i + 1) + "]";
      if (this.findElement(newPath) == e) {
        return newPath;
      }
    }
  }
  return xpath;
};

/*
 * ===== builders =====
 */

LocatorBuilders.add("id", function(e) {
  if (e.id) {
    return "id=" + e.id;
  }
  return null;
});

LocatorBuilders.add("linkText", function(e) {
  if (e.nodeName == "A") {
    let text = e.textContent;
    if (!text.match(/^\s*$/)) {
      return "linkText=" + text.replace(/\xA0/g, " ").replace(/^\s*(.*?)\s*$/, "$1");
    }
  }
  return null;
});

LocatorBuilders.add("name", function(e) {
  if (e.name) {
    return "name=" + e.name;
  }
  return null;
});

LocatorBuilders.add("css", function(e) {
  let current = e;
  let sub_path = this.getCSSSubPath(e);
  while (this.findElement("css=" + sub_path) != e && current.nodeName.toLowerCase() != "html") {
    sub_path = this.getCSSSubPath(current.parentNode) + " > " + sub_path;
    current = current.parentNode;
  }
  return "css=" + sub_path;
});

LocatorBuilders.add("css:finder", function(e) {
  return "css=" + finder(e);
});

LocatorBuilders.add("xpath:link", function(e) {
  if (e.nodeName == "A") {
    let text = e.textContent;
    if (!text.match(/^\s*$/)) {
      return this.preciseXPath("//" + this.xpathHtmlElement("a") + "[contains(text(),'" + text.replace(/^\s+/, "").replace(/\s+$/, "") + "')]", e);
    }
  }
  return null;
});

LocatorBuilders.add("xpath:img", function(e) {
  if (e.nodeName == "IMG") {
    if (e.alt != "") {
      return this.preciseXPath("//" + this.xpathHtmlElement("img") + "[@alt=" + this.attributeValue(e.alt) + "]", e);
    } else if (e.title != "") {
      return this.preciseXPath("//" + this.xpathHtmlElement("img") + "[@title=" + this.attributeValue(e.title) + "]", e);
    } else if (e.src != "") {
      return this.preciseXPath("//" + this.xpathHtmlElement("img") + "[contains(@src," + this.attributeValue(e.src) + ")]", e);
    }
  }
  return null;
});

LocatorBuilders.add("xpath:attributes", function(e) {
  const PREFERRED_ATTRIBUTES = ["id", "name", "value", "type", "action", "onclick"];
  let i = 0;

  function attributesXPath(name, attNames, attributes) {
    let locator = "//" + this.xpathHtmlElement(name) + "[";
    for (i = 0; i < attNames.length; i++) {
      if (i > 0) {
        locator += " and ";
      }
      let attName = attNames[i];
      locator += "@" + attName + "=" + this.attributeValue(attributes[attName]);
    }
    locator += "]";
    return this.preciseXPath(locator, e);
  }

  if (e.attributes) {
    let atts = e.attributes;
    let attsMap = {};
    for (i = 0; i < atts.length; i++) {
      let att = atts[i];
      attsMap[att.name] = att.value;
    }
    let names = [];
    // try preferred attributes
    for (i = 0; i < PREFERRED_ATTRIBUTES.length; i++) {
      let name = PREFERRED_ATTRIBUTES[i];
      if (attsMap[name] != null) {
        names.push(name);
        let locator = attributesXPath.call(this, e.nodeName.toLowerCase(), names, attsMap);
        if (e == this.findElement(locator)) {
          return locator;
        }
      }
    }
  }
  return null;
});

LocatorBuilders.add("xpath:idRelative", function(e) {
  let path = "";
  let current = e;
  while (current != null) {
    if (current.parentNode != null) {
      path = this.relativeXPathFromParent(current) + path;
      if (1 == current.parentNode.nodeType && // ELEMENT_NODE
        current.parentNode.getAttribute("id")) {
        return this.preciseXPath("//" + this.xpathHtmlElement(current.parentNode.nodeName.toLowerCase()) +
          "[@id=" + this.attributeValue(current.parentNode.getAttribute("id")) + "]" +
          path, e);
      }
    } else {
      return null;
    }
    current = current.parentNode;
  }
  return null;
});

LocatorBuilders.add("xpath:href", function(e) {
  if (e.attributes && e.hasAttribute("href")) {
    let href = e.getAttribute("href");
    if (href.search(/^http?:\/\//) >= 0) {
      return this.preciseXPath("//" + this.xpathHtmlElement("a") + "[@href=" + this.attributeValue(href) + "]", e);
    } else {
      // use contains(), because in IE getAttribute("href") will return absolute path
      return this.preciseXPath("//" + this.xpathHtmlElement("a") + "[contains(@href, " + this.attributeValue(href) + ")]", e);
    }
  }
  return null;
});

LocatorBuilders.add("xpath:position", function(e, opt_contextNode) {
  //this.log.debug("positionXPath: e=" + e);
  let path = "";
  let current = e;
  while (current != null && current != opt_contextNode) {
    let currentPath;
    if (current.parentNode != null) {
      currentPath = this.relativeXPathFromParent(current);
    } else {
      currentPath = "/" + this.xpathHtmlElement(current.nodeName.toLowerCase());
    }
    path = currentPath + path;
    let locator = "/" + path;
    if (e == this.findElement(locator)) {
      return locator;
    }
    current = current.parentNode;
    //this.log.debug("positionXPath: current=" + current);
  }
  return null;
});
// Samit: Warning: The old method of setting the order using LocatorBuilders.order is now deprecated
// You can change the priority of builders by setting LocatorBuilders.order.
//LocatorBuilders.order = ['id', 'link', 'name', 'css', 'xpath:link', 'xpath:img', 'xpath:attributes', 'xpath:idRelative', 'xpath:href', 'xpath:position'];
<commit_msg>Fix for the locator builder so xpath locators are generated with a `xpath=` prefix. This prevents the warning of `Warning implicit locators are deprecated, please change the locator to xpath=locator` from showing up in the logs on locator fallback.<commit_after>/*
 * Copyright 2005 Shinya Kasatani
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

//import { MozillaBrowserBot } from "./selenium-browserbot";
import { bot, core } from "./closure-polyfill";
import { parse_locator } from "./utils";
import finder from "@medv/finder";

export default function LocatorBuilders(window) {
  this.window = window;
}

window.LocatorBuilders = LocatorBuilders;

LocatorBuilders.prototype.detach = function() {
};

LocatorBuilders.prototype.buildWith = function(name, e, opt_contextNode) {
  return LocatorBuilders.builderMap[name].call(this, e, opt_contextNode);
};

LocatorBuilders.prototype.elementEquals = function(name, e, locator) {
  let fe = this.findElement(locator);
  //TODO: add match function to the ui locator builder, note the inverted parameters
  return (e == fe) || (LocatorBuilders.builderMap[name] && LocatorBuilders.builderMap[name].match && LocatorBuilders.builderMap[name].match(e, fe));
};

LocatorBuilders.prototype.build = function(e) {
  let locators = this.buildAll(e);
  if (locators.length > 0) {
    return locators[0][0];
  } else {
    return "LOCATOR_DETECTION_FAILED";
  }
};

LocatorBuilders.prototype.buildAll = function(el) {
  let e = core.firefox.unwrap(el); //Samit: Fix: Do the magic to get it to work in Firefox 4
  let locator;
  let locators = [];
  for (let i = 0; i < LocatorBuilders.order.length; i++) {
    let finderName = LocatorBuilders.order[i];
    try {
      locator = this.buildWith(finderName, e);
      if (locator) {
        locator = String(locator);
        //Samit: The following is a quickfix for above commented code to stop exceptions on almost every locator builder
        //TODO: the builderName should NOT be used as a strategy name, create a feature to allow locatorBuilders to specify this kind of behaviour
        //TODO: Useful if a builder wants to capture a different element like a parent. Use the this.elementEquals
        let fe = this.findElement(locator);
        if (e == fe) {
          locators.push([locator, finderName]);
        }
      }
    } catch (e) {
      // TODO ignore the buggy locator builder for now
      //this.log.debug("locator exception: " + e);
    }
  }
  return locators;
};

LocatorBuilders.prototype.findElement = function(loc) {
  try {
    const locator = parse_locator(loc, true);
    return bot.locators.findElement({ [locator.type]: locator.string }, this.window.document);
  } catch (error) {
    //this.log.debug("findElement failed: " + error + ", locator=" + locator);
    return null;
  }
};

/*
 * Class methods
 */

LocatorBuilders.order = [];
LocatorBuilders.builderMap = {};
LocatorBuilders._preferredOrder = [];
// NOTE: for some reasons we does not use this part
// classObservable(LocatorBuilders);

LocatorBuilders.add = function(name, finder) {
  this.order.push(name);
  this.builderMap[name] = finder;
  this._orderChanged();
};

/**
 * Call when the order or preferred order changes
 */
LocatorBuilders._orderChanged = function() {
  let changed = this._ensureAllPresent(this.order, this._preferredOrder);
  this._sortByRefOrder(this.order, this._preferredOrder);
  if (changed) {
    // NOTE: for some reasons we does not use this part
    // this.notify('preferredOrderChanged', this._preferredOrder);
  }
};

/**
 * Set the preferred order of the locator builders
 *
 * @param preferredOrder can be an array or a comma separated string of names
 */
LocatorBuilders.setPreferredOrder = function(preferredOrder) {
  if (typeof preferredOrder === "string") {
    this._preferredOrder = preferredOrder.split(",");
  } else {
    this._preferredOrder = preferredOrder;
  }
  this._orderChanged();
};

/**
 * Returns the locator builders preferred order as an array
 */
LocatorBuilders.getPreferredOrder = function() {
  return this._preferredOrder;
};

/**
 * Sorts arrayToSort in the order of elements in sortOrderReference
 * @param arrayToSort
 * @param sortOrderReference
 */
LocatorBuilders._sortByRefOrder = function(arrayToSort, sortOrderReference) {
  let raLen = sortOrderReference.length;
  arrayToSort.sort(function(a, b) {
    let ai = sortOrderReference.indexOf(a);
    let bi = sortOrderReference.indexOf(b);
    return (ai > -1 ? ai : raLen) - (bi > -1 ? bi : raLen);
  });
};

/**
 * Function to add to the bottom of destArray elements from source array that do not exist in destArray
 * @param sourceArray
 * @param destArray
 */
LocatorBuilders._ensureAllPresent = function(sourceArray, destArray) {
  let changed = false;
  sourceArray.forEach(function(e) {
    if (destArray.indexOf(e) == -1) {
      destArray.push(e);
      changed = true;
    }
  });
  return changed;
};

/*
 * Utility function: Encode XPath attribute value.
 */
LocatorBuilders.prototype.attributeValue = function(value) {
  if (value.indexOf("'") < 0) {
    return "'" + value + "'";
  } else if (value.indexOf("\"") < 0) {
    return "\"" + value + "\"";
  } else {
    let result = "concat(";
    let part = "";
    let didReachEndOfValue = false;
    while (!didReachEndOfValue) {
      let apos = value.indexOf("'");
      let quot = value.indexOf("\"");
      if (apos < 0) {
        result += "'" + value + "'";
        didReachEndOfValue = true;
        break;
      } else if (quot < 0) {
        result += "\"" + value + "\"";
        didReachEndOfValue = true;
        break;
      } else if (quot < apos) {
        part = value.substring(0, apos);
        result += "'" + part + "'";
        value = value.substring(part.length);
      } else {
        part = value.substring(0, quot);
        result += "\"" + part + "\"";
        value = value.substring(part.length);
      }
      result += ",";
    }
    result += ")";
    return result;
  }
};

LocatorBuilders.prototype.xpathHtmlElement = function(name) {
  if (this.window.document.contentType == "application/xhtml+xml") {
    // "x:" prefix is required when testing XHTML pages
    return "x:" + name;
  } else {
    return name;
  }
};

LocatorBuilders.prototype.relativeXPathFromParent = function(current) {
  let index = this.getNodeNbr(current);
  let currentPath = "/" + this.xpathHtmlElement(current.nodeName.toLowerCase());
  if (index > 0) {
    currentPath += "[" + (index + 1) + "]";
  }
  return currentPath;
};

LocatorBuilders.prototype.getNodeNbr = function(current) {
  let childNodes = current.parentNode.childNodes;
  let total = 0;
  let index = -1;
  for (let i = 0; i < childNodes.length; i++) {
    let child = childNodes[i];
    if (child.nodeName == current.nodeName) {
      if (child == current) {
        index = total;
      }
      total++;
    }
  }
  return index;
};

LocatorBuilders.prototype.getCSSSubPath = function(e) {
  let css_attributes = ["id", "name", "class", "type", "alt", "title", "value"];
  for (let i = 0; i < css_attributes.length; i++) {
    let attr = css_attributes[i];
    let value = e.getAttribute(attr);
    if (value) {
      if (attr == "id")
        return "#" + value;
      if (attr == "class")
        return e.nodeName.toLowerCase() + "." + value.replace(" ", ".").replace("..", ".");
      return e.nodeName.toLowerCase() + "[" + attr + "=\"" + value + "\"]";
    }
  }
  if (this.getNodeNbr(e))
    return e.nodeName.toLowerCase() + ":nth-of-type(" + this.getNodeNbr(e) + ")";
  else
    return e.nodeName.toLowerCase();
};

LocatorBuilders.prototype.preciseXPath = function(xpath, e) {
  //only create more precise xpath if needed
  if (this.findElement(xpath) != e) {
    let result = e.ownerDocument.evaluate(xpath, e.ownerDocument, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    //skip first element (result:0 xpath index:1)
    for (let i = 0, len = result.snapshotLength; i < len; i++) {
      let newPath = "xpath=(" + xpath + ")[" + (i + 1) + "]";
      if (this.findElement(newPath) == e) {
        return newPath;
      }
    }
  }
  return "xpath=" + xpath;
};

/*
 * ===== builders =====
 */

LocatorBuilders.add("id", function(e) {
  if (e.id) {
    return "id=" + e.id;
  }
  return null;
});

LocatorBuilders.add("linkText", function(e) {
  if (e.nodeName == "A") {
    let text = e.textContent;
    if (!text.match(/^\s*$/)) {
      return "linkText=" + text.replace(/\xA0/g, " ").replace(/^\s*(.*?)\s*$/, "$1");
    }
  }
  return null;
});

LocatorBuilders.add("name", function(e) {
  if (e.name) {
    return "name=" + e.name;
  }
  return null;
});

LocatorBuilders.add("css", function(e) {
  let current = e;
  let sub_path = this.getCSSSubPath(e);
  while (this.findElement("css=" + sub_path) != e && current.nodeName.toLowerCase() != "html") {
    sub_path = this.getCSSSubPath(current.parentNode) + " > " + sub_path;
    current = current.parentNode;
  }
  return "css=" + sub_path;
});

LocatorBuilders.add("css:finder", function(e) {
  return "css=" + finder(e);
});

LocatorBuilders.add("xpath:link", function(e) {
  if (e.nodeName == "A") {
    let text = e.textContent;
    if (!text.match(/^\s*$/)) {
      return this.preciseXPath("//" + this.xpathHtmlElement("a") + "[contains(text(),'" + text.replace(/^\s+/, "").replace(/\s+$/, "") + "')]", e);
    }
  }
  return null;
});

LocatorBuilders.add("xpath:img", function(e) {
  if (e.nodeName == "IMG") {
    if (e.alt != "") {
      return this.preciseXPath("//" + this.xpathHtmlElement("img") + "[@alt=" + this.attributeValue(e.alt) + "]", e);
    } else if (e.title != "") {
      return this.preciseXPath("//" + this.xpathHtmlElement("img") + "[@title=" + this.attributeValue(e.title) + "]", e);
    } else if (e.src != "") {
      return this.preciseXPath("//" + this.xpathHtmlElement("img") + "[contains(@src," + this.attributeValue(e.src) + ")]", e);
    }
  }
  return null;
});

LocatorBuilders.add("xpath:attributes", function(e) {
  const PREFERRED_ATTRIBUTES = ["id", "name", "value", "type", "action", "onclick"];
  let i = 0;

  function attributesXPath(name, attNames, attributes) {
    let locator = "//" + this.xpathHtmlElement(name) + "[";
    for (i = 0; i < attNames.length; i++) {
      if (i > 0) {
        locator += " and ";
      }
      let attName = attNames[i];
      locator += "@" + attName + "=" + this.attributeValue(attributes[attName]);
    }
    locator += "]";
    return this.preciseXPath(locator, e);
  }

  if (e.attributes) {
    let atts = e.attributes;
    let attsMap = {};
    for (i = 0; i < atts.length; i++) {
      let att = atts[i];
      attsMap[att.name] = att.value;
    }
    let names = [];
    // try preferred attributes
    for (i = 0; i < PREFERRED_ATTRIBUTES.length; i++) {
      let name = PREFERRED_ATTRIBUTES[i];
      if (attsMap[name] != null) {
        names.push(name);
        let locator = attributesXPath.call(this, e.nodeName.toLowerCase(), names, attsMap);
        if (e == this.findElement(locator)) {
          return locator;
        }
      }
    }
  }
  return null;
});

LocatorBuilders.add("xpath:idRelative", function(e) {
  let path = "";
  let current = e;
  while (current != null) {
    if (current.parentNode != null) {
      path = this.relativeXPathFromParent(current) + path;
      if (1 == current.parentNode.nodeType && // ELEMENT_NODE
        current.parentNode.getAttribute("id")) {
        return this.preciseXPath("//" + this.xpathHtmlElement(current.parentNode.nodeName.toLowerCase()) +
          "[@id=" + this.attributeValue(current.parentNode.getAttribute("id")) + "]" +
          path, e);
      }
    } else {
      return null;
    }
    current = current.parentNode;
  }
  return null;
});

LocatorBuilders.add("xpath:href", function(e) {
  if (e.attributes && e.hasAttribute("href")) {
    let href = e.getAttribute("href");
    if (href.search(/^http?:\/\//) >= 0) {
      return this.preciseXPath("//" + this.xpathHtmlElement("a") + "[@href=" + this.attributeValue(href) + "]", e);
    } else {
      // use contains(), because in IE getAttribute("href") will return absolute path
      return this.preciseXPath("//" + this.xpathHtmlElement("a") + "[contains(@href, " + this.attributeValue(href) + ")]", e);
    }
  }
  return null;
});

LocatorBuilders.add("xpath:position", function(e, opt_contextNode) {
  let path = "";
  let current = e;
  while (current != null && current != opt_contextNode) {
    let currentPath;
    if (current.parentNode != null) {
      currentPath = this.relativeXPathFromParent(current);
    } else {
      currentPath = "/" + this.xpathHtmlElement(current.nodeName.toLowerCase());
    }
    path = currentPath + path;
    let locator = "/" + path;
    if (e == this.findElement(locator)) {
      return "xpath=" + locator;
    }
    current = current.parentNode;
  }
  return null;
});
// Samit: Warning: The old method of setting the order using LocatorBuilders.order is now deprecated
// You can change the priority of builders by setting LocatorBuilders.order.
//LocatorBuilders.order = ['id', 'link', 'name', 'css', 'xpath:link', 'xpath:img', 'xpath:attributes', 'xpath:idRelative', 'xpath:href', 'xpath:position'];
<|endoftext|>


<commit_before>"use strict";
/*!
 * Module dependencies.
 */

var
  hotplate = require('hotplate')
, path = require('path')
, async = require('async')
, hotCoreStoreRegistry = require('hotCoreStoreRegistry')
;

var getConfigRecords = exports.getConfigRecords = function( workspaceId, userId, cb ){

  // Sanity check: drop the workspaceId parameter if multiHome is not defined or false
  if( ! hotplate.config.get('hotCoreMultiHome.enabled') ){
    workspaceId = null;
  }

  var functionList = {};

  getConfigStores( function( err, configStores ){

    Object.keys( configStores.stores ).forEach( function( storeName ){

        function loadConfigStoreFunction( Store, storeObject ){

          return function( next ){
            hotplate.log("PRE-LOADING RECORD FOR " + storeObject.storeName );

            var filter = {};

            if( storeObject.configStore.workspaceId ){
              filter.workspaceId = workspaceId;
            }
            if( storeObject.configStore.userId ){
              filter.userId = userId;
            }

            // It's a one-field set, it will only return ONE value
            if( storeObject.idProperty == 'workspaceId' || storeObject.idProperty == 'userId' ){

              hotplate.log("ONE-RECORD config store -- getting it (or creating it if missing)");
              Store.GetQuery( { filters: filter }, function( err, docs ){
                if( err ) {
                   next( err );
                } else {
                  if( docs.length !== 0 ){
                    hotplate.log("Record existed, all good for " + storeObject.storeName );
                    next( null, docs[ 0 ] );
                  } else {
                    hotplate.log("Record didn't exist! Creating one for " + storeObject.storeName );
                    hotplate.log(filter );
                    Store.Put( null, filter, { killComet: true }, function( err, doc ){
                      next( err, doc );
                    })
                  }
                }
              });

            } else {
              hotplate.log("COLLECTION config store -- getting it");
              Store.GetQuery( { filters: filter }, function( err, docs ){
                next( err, docs );
              });
            }

          }
        }

      var storeEntry = configStores.stores[ storeName ];

      var Store = storeEntry.Store;
      var storeObject = storeEntry.storeObject;

      // The store is a config store: magic happens
      if( storeObject.configStore ){

        var pushFunction = true;
        if( storeObject.configStore.workspaceId && ! workspaceId ) pushFunction = false;
        if( storeObject.configStore.userId && ! userId ) pushFunction = false;

        // Pre-load the values for that store -- those values will then be shown
        // as objects in the page, so that the application won't have to make a Json REST call to fetch them.
        if( pushFunction ){
          functionList[ storeName ] = loadConfigStoreFunction( Store, storeObject );
        }

      }
    });
  });

  async.series( functionList, function( err, docs ){
    cb( err, docs );
  });

}

hotplate.hotEvents.on( 'pageElements', 'hotCoreStoreConfig', hotplate.cachable( function( done ){

  var results = {};
  exports.getConfigStores( function( err, configStores ){
    if( err ){
      done( err );
    } else {

      Object.keys( configStores.stores ).forEach( function( storeName ){
        var storeEntry = configStores.stores[ storeName ];

        var Store = storeEntry.Store;
        var storeObject = storeEntry.storeObject;

        if( storeObject.publicURL ){
         results[ storeObject.storeName ] = { workspaceId: !!storeObject.configStore.workspaceId, userId: !!storeObject.configStore.userId };
        }
      });

      done( null, {
        vars:  [ { name: 'configStores', value: results } ],
      });

    }

  });
}));

hotplate.hotEvents.on( 'pageElementsPerPage', 'hotCoreStoreConfig', function( req, pageName, done ){

  getConfigRecords( req.params.workspaceId, req.session.userId, function( err, configRecords ){
    if( err ){
      done( err );
    } else {
      done( null, {
        vars:  [ { name: 'storeRecords', value: configRecords } ],
      } );
    }
  });

});

var getConfigStores = exports.getConfigStores = function( done ){

   var ret = {
    stores: {},
    collections: {},
  };

  hotCoreStoreRegistry.getAllStores( function( err, allStores ){
    if( err ){
      done( err );
    } else {
      Object.keys( allStores.stores ).forEach( function( storeName ){
        var storeEntry = allStores.stores[ storeName ];

        var Store = storeEntry.Store;
        var storeObject = storeEntry.storeObject;

        // The store is a config store: add it to the list to be returned
        if( storeObject.configStore ){
          ret.stores[ storeName ] = allStores.stores[ storeName ];

          ret.collections[ storeObject.collectionName ] = ret.collections[ storeObject.collectionName ] || [];
          ret.collections[ storeObject.collectionName ].push( { Store: Store, storeObject: storeObject }  );
        }
      });
    }
  });

  // Load the lot, run done() at the end
  done( null, ret );
}

<commit_msg>Made it possible to have required for config fields<commit_after>"use strict";
/*!
 * Module dependencies.
 */

var
  hotplate = require('hotplate')
, path = require('path')
, async = require('async')
, hotCoreStoreRegistry = require('hotCoreStoreRegistry')
;

var getConfigRecords = exports.getConfigRecords = function( workspaceId, userId, cb ){

  // Sanity check: drop the workspaceId parameter if multiHome is not defined or false
  if( ! hotplate.config.get('hotCoreMultiHome.enabled') ){
    workspaceId = null;
  }

  var functionList = {};

  getConfigStores( function( err, configStores ){

    Object.keys( configStores.stores ).forEach( function( storeName ){

      function loadConfigStoreFunction( Store, storeObject ){

        return function( next ){
          hotplate.log("PRE-LOADING RECORD FOR " + storeObject.storeName );

          var filter = {};

          if( storeObject.configStore.workspaceId ){
            filter.workspaceId = workspaceId;
          }
          if( storeObject.configStore.userId ){
            filter.userId = userId;
          }

          // It's a one-field set, it will only return ONE value
          if( storeObject.idProperty == 'workspaceId' || storeObject.idProperty == 'userId' ){

            hotplate.log("ONE-RECORD config store -- getting it (or creating it if missing)");
            Store.GetQuery( { filters: filter }, function( err, docs ){
              if( err ) {
                 next( err );
              } else {
                if( docs.length !== 0 ){
                  hotplate.log("Record existed, all good for " + storeObject.storeName );
                  next( null, docs[ 0 ] );
                } else {
                  hotplate.log("Record didn't exist! Creating one for " + storeObject.storeName );
                  hotplate.log(filter );

                  // Adding defaults taken from the schema
                  for( var k in storeObject.schema.structure ){
                    var v = storeObject.schema.structure[ k ];
                    if( v.default ) filter[ k ] = v.default;
                  }

                  // Actually do the put
                  Store.Put( null, filter, { killComet: true }, function( err, doc ){
                    next( err, doc );
                  })
                }
              }
            });

          } else {
            hotplate.log("COLLECTION config store -- getting it");
            Store.GetQuery( { filters: filter }, function( err, docs ){
              next( err, docs );
            });
          }

        }
      }

      var storeEntry = configStores.stores[ storeName ];

      var Store = storeEntry.Store;
      var storeObject = storeEntry.storeObject;

      // The store is a config store: magic happens
      if( storeObject.configStore ){

        var pushFunction = true;
        if( storeObject.configStore.workspaceId && ! workspaceId ) pushFunction = false;
        if( storeObject.configStore.userId && ! userId ) pushFunction = false;

        // Pre-load the values for that store -- those values will then be shown
        // as objects in the page, so that the application won't have to make a Json REST call to fetch them.
        if( pushFunction ){
          functionList[ storeName ] = loadConfigStoreFunction( Store, storeObject );
        }

      }
    });
  });

  async.series( functionList, function( err, docs ){
    cb( err, docs );
  });

}

hotplate.hotEvents.on( 'pageElements', 'hotCoreStoreConfig', hotplate.cachable( function( done ){

  var results = {};
  exports.getConfigStores( function( err, configStores ){
    if( err ){
      done( err );
    } else {

      Object.keys( configStores.stores ).forEach( function( storeName ){
        var storeEntry = configStores.stores[ storeName ];

        var Store = storeEntry.Store;
        var storeObject = storeEntry.storeObject;

        if( storeObject.publicURL ){
         results[ storeObject.storeName ] = { workspaceId: !!storeObject.configStore.workspaceId, userId: !!storeObject.configStore.userId };
        }
      });

      done( null, {
        vars:  [ { name: 'configStores', value: results } ],
      });

    }

  });
}));

hotplate.hotEvents.on( 'pageElementsPerPage', 'hotCoreStoreConfig', function( req, pageName, done ){

  getConfigRecords( req.params.workspaceId, req.session.userId, function( err, configRecords ){
    if( err ){
      done( err );
    } else {
      done( null, {
        vars:  [ { name: 'storeRecords', value: configRecords } ],
      } );
    }
  });

});

var getConfigStores = exports.getConfigStores = function( done ){

   var ret = {
    stores: {},
    collections: {},
  };

  hotCoreStoreRegistry.getAllStores( function( err, allStores ){
    if( err ){
      done( err );
    } else {
      Object.keys( allStores.stores ).forEach( function( storeName ){
        var storeEntry = allStores.stores[ storeName ];

        var Store = storeEntry.Store;
        var storeObject = storeEntry.storeObject;

        // The store is a config store: add it to the list to be returned
        if( storeObject.configStore ){
          ret.stores[ storeName ] = allStores.stores[ storeName ];

          ret.collections[ storeObject.collectionName ] = ret.collections[ storeObject.collectionName ] || [];
          ret.collections[ storeObject.collectionName ].push( { Store: Store, storeObject: storeObject }  );
        }
      });
    }
  });

  // Load the lot, run done() at the end
  done( null, ret );
}

<|endoftext|>


<commit_before>/**
 * @package @cyrhla/tester
 * @license MIT
 * @copyright Krystian Pietruszka <kpietru@cyrhla.com>
 * @see http://www.cyrhla.com
 */

'use strict'

const Tester = require('@cyrhla/tester/Tester')
const valid = require('../valid')

/**
 * validTest
 *
 * @author Krystian Pietruszka <kpietru@cyrhla.com>
 */
module.exports = class validTest extends Tester
{
    testValidInvalidTypeError()
    {
        this.expectError('TypeError', function() {
            valid(123, 'string')
        })
        this.expectError('TypeError', function() {
            valid(0, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array')
        })
        this.expectError('TypeError', function() {
            valid(null, 'string', 'boolean', 'function', 'object', 'regexp', 'number', 'array')
        })
        this.expectError('TypeError', function() {
            valid(false, 'string', 'null', 'function', 'object', 'regexp', 'number', 'array')
        })
        this.expectError('TypeError', function() {
            valid(Object, 'string', 'boolean', 'null', 'object', 'regexp', 'number', 'array')
        })
        this.expectError('TypeError', function() {
            valid(new Object(), 'string', 'boolean', 'null', 'function', 'regexp', 'number', 'array')
        })
        this.expectError('TypeError', function() {
            valid([], 'string', 'boolean', 'null', 'object', 'regexp', 'number', 'function')
        })
        this.expectError('TypeError', function() {
            valid('', 'function', 'boolean', 'null', 'object', 'regexp', 'number', 'array')
        })
        this.expectError('TypeError', function() {
            valid(Symbol('foo'), 'function', 'boolean', 'null', 'object', 'regexp', 'number', 'array')
        })
    }

    testValidReturnsSame()
    {
        this.assertSame(123, valid(123, 'number'))
        this.assertSame('', valid('', 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol'))
        this.assertSame(0, valid(0, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol'))
        this.assertSame(null, valid(null, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol'))
        this.assertSame(false, valid(false, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol'))
        this.assertSame(Object, valid(Object, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol'))
        this.assertSame(new Object(), valid(new Object(), 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol'))
        this.assertSame([], valid([], 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol'))
        this.assertSame(/^/, valid(/^/, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol'))
    }
}
<commit_msg>Update validTest.js<commit_after>/**
 * @package @cyrhla/tester
 * @license MIT
 * @copyright Krystian Pietruszka <kpietru@cyrhla.com>
 * @see http://www.cyrhla.com
 */

'use strict'

const Tester = require('@cyrhla/tester/Tester')
const valid = require('../valid')

/**
 * validTest
 *
 * @author Krystian Pietruszka <kpietru@cyrhla.com>
 */
module.exports = class validTest extends Tester
{
    testValidInvalidTypeError()
    {
        this.expectError('TypeError', function() {
            valid(123, 'string', 'undefined')
        })
        this.expectError('TypeError', function() {
            valid(0, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'undefined')
        })
        this.expectError('TypeError', function() {
            valid(null, 'string', 'boolean', 'function', 'object', 'regexp', 'number', 'array', 'undefined')
        })
        this.expectError('TypeError', function() {
            valid(false, 'string', 'null', 'function', 'object', 'regexp', 'number', 'array', 'undefined')
        })
        this.expectError('TypeError', function() {
            valid(Object, 'string', 'boolean', 'null', 'object', 'regexp', 'number', 'array', 'undefined')
        })
        this.expectError('TypeError', function() {
            valid(new Object(), 'string', 'boolean', 'null', 'function', 'regexp', 'number', 'array', 'undefined')
        })
        this.expectError('TypeError', function() {
            valid([], 'string', 'boolean', 'null', 'object', 'regexp', 'number', 'function', 'undefined')
        })
        this.expectError('TypeError', function() {
            valid('', 'function', 'boolean', 'null', 'object', 'regexp', 'number', 'array', 'undefined')
        })
        this.expectError('TypeError', function() {
            valid(Symbol('foo'), 'function', 'boolean', 'null', 'object', 'regexp', 'number', 'array', 'undefined')
        })
        this.expectError('TypeError', function() {
            valid(undefined, 'function', 'boolean', 'null', 'object', 'regexp', 'number', 'array')
        })
    }

    testValidReturnsSame()
    {
        this.assertSame(123, valid(123, 'number'))
        this.assertSame('', valid('', 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol', 'undefined'))
        this.assertSame(0, valid(0, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol', 'undefined'))
        this.assertSame(null, valid(null, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol', 'undefined'))
        this.assertSame(false, valid(false, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol', 'undefined'))
        this.assertSame(Object, valid(Object, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol', 'undefined'))
        this.assertSame(new Object(), valid(new Object(), 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol', 'undefined'))
        this.assertSame([], valid([], 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol', 'undefined'))
        this.assertSame(/^/, valid(/^/, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol', 'undefined'))
        this.assertSame(undefined, valid(undefined, 'string', 'boolean', 'function', 'object', 'regexp', 'null', 'array', 'number', 'symbol', 'undefined'))
    }
}
<|endoftext|>


<commit_before>import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { SprkButton } from '@sparkdesignsystem/spark-core-react';
import { uniqueId } from 'lodash';

const SprkTable = (props) => {

  const {
    data,
    columns,
    variant,
    idString,
    additionalClasses,
    additionalTableClasses,
    children,
    ...other
  } = props;

  const wrapperClassNames = classnames(
    'sprk-b-TableContainer',
    additionalClasses
  );

  const tableClassNames = classnames(
    'sprk-b-Table',
    {'sprk-b-Table--secondary' : variant === "secondary"},
    {'sprk-b-Table--secondary-row-comparison' : variant === "secondaryRowComparison"},
    {'sprk-b-Table--grouped-columns' : variant === "grouped"},
    {'sprk-b-Table--row-comparison' : variant === 'rowComparison'},
    additionalTableClasses
  );

  if(variant === 'default' || variant === 'secondary') {
    return (

            {columns &&

              {columns.map(col => {
                return {col.header}
              })}

            }

          {data &&

          {data.map(row =>

              {columns.map( col =>
                {row[col.name]}
              )}

          )}

          }

    );
  }

  if(variant === 'grouped') {
    return (

            {columns &&
             columns.map(row =>

                {row.map(col =>
                  {col.header}
                )}

            )
            }

          {data &&

            {data.map(row =>

                {
                  Object.values(row).map(item =>
                    {item}
                  )
                }

            )}

          }

    )
  }

  if(variant === 'rowComparison') {
    return (

              {columns.map(col => {
                return {col.header}
              })}

          {data &&

          {data.map(row =>

                {row.rowHeading}
              {columns.map( col =>
                {row[col.name]}
              )}

          )}

          }

    );
  }

  if(variant === 'secondaryRowComparison') {

    return (

          { data &&

            {data.map(row =>

              {row.rowHeading}
              {columns.map( col =>
              {row[col.name]}
              )}

                {row.button}

            )}

          }

    )
  }

  if(variant === 'html') {
    return (

          {children}

    )
  }

}

SprkTable.propTypes = {
  // An array used to render the table headings
  columns: PropTypes.array,
  // An array of objects used to map and render the table row data
  data: PropTypes.arrayOf(PropTypes.object),
  // A string to determine what variant of the table should render
  variant: PropTypes.oneOf(['default', 'secondary', 'grouped', 'rowComparison', 'secondaryRowComparison', 'html']),
  // The string to use for the data-id attribute
  idString: PropTypes.string,
  // Any additional classes to add to the table wrapper
  additionalClasses: PropTypes.string,
  // Any additional classes to add to the table
  additionalTableClasses: PropTypes.string,
  // And children markup to be rendered
  children: PropTypes.node,
}

SprkTable.defaultProps = {
  columns: [],
  data: [],
  variant: 'default',
  idString: '',
  additionalClasses: '',
  additionalTableClasses: '',
}

export default SprkTable;<commit_msg>fix eslint errors<commit_after>import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import { uniqueId } from 'lodash';

const SprkTable = (props) => {
  const {
    data,
    columns,
    variant,
    idString,
    additionalClasses,
    additionalTableClasses,
    children,
    ...other
  } = props;

  const wrapperClassNames = classnames(
    'sprk-b-TableContainer',
    additionalClasses,
  );

  const tableClassNames = classnames(
    'sprk-b-Table',
    { 'sprk-b-Table--secondary': variant === 'secondary' },
    { 'sprk-b-Table--secondary-row-comparison': variant === 'secondaryRowComparison' },
    { 'sprk-b-Table--grouped-columns': variant === 'grouped' },
    { 'sprk-b-Table--row-comparison': variant === 'rowComparison' },
    additionalTableClasses,
  );

  if (variant === 'default' || variant === 'secondary') {
    return (

            {columns
            && (

              {columns.map(col => {col.header}	)}

            )
            }

          {data
          && (

            {data.map(row => (

                {columns.map(col => {row[col.name]}	)}

            ))}

          )
          }

    );
  }

  if (variant === 'grouped') {
    return (

            {columns
            && (
              columns.map(row => (

                  {row.map(col => {col.header}	)}

              ))
            )
            }

          {data
          && (

            {data.map(row => (

                {
                  Object.values(row).map(item => {item}	)
                }

            ))}

          )
          }

    );
  }

  if (variant === 'rowComparison') {
    return (

              {columns.map(col => {col.header}	)}

          {data
          && (

            {data.map(row => (

                {row.rowHeading}
                {columns.map(col => {row[col.name]}	)}

            ))}

          )
          }

    );
  }

  if (variant === 'secondaryRowComparison') {
    return (

          {data
          && (

            {data.map(row => (

                {row.rowHeading}
                {columns.map(col => {row[col.name]}	)}

                  {row.button}

            ))}

          )
          }

    );
  }

  if (variant === 'html') {
    return (

          {children}

    );
  }
  return (null);
};

SprkTable.propTypes = {
  // An array used to render the table headings - Can be an array of objects or an array of arrays
  columns: PropTypes.oneOfType([
    PropTypes.arrayOf(PropTypes.object),
    PropTypes.arrayOf(PropTypes.array),
  ]),
  // An array of objects used to map and render the table row data
  data: PropTypes.arrayOf(PropTypes.object),
  // A string to determine what variant of the table should render
  variant: PropTypes.oneOf(['default', 'secondary', 'grouped', 'rowComparison', 'secondaryRowComparison', 'html']),
  // The string to use for the data-id attribute
  idString: PropTypes.string,
  // Any additional classes to add to the table wrapper
  additionalClasses: PropTypes.string,
  // Any additional classes to add to the table
  additionalTableClasses: PropTypes.string,
  // And children markup to be rendered
  children: PropTypes.node,
};

SprkTable.defaultProps = {
  columns: [],
  data: [],
  variant: 'default',
  idString: '',
  additionalClasses: '',
  additionalTableClasses: '',
  children: undefined,
};

export default SprkTable;
<|endoftext|>


<commit_before>import React from 'react';
import Backbone from 'backbone';
import BootstrapModalMixin from 'components/mixins/BootstrapModalMixin.react';
import stores from 'stores';
import InstanceSelect from '../volume_attach/InstanceSelect.react';

// Example Usage from http://bl.ocks.org/insin/raw/8449696/
// render: function(){
//

//   ...custom components...
//   <ExampleModal
//      ref="modal"
//      show={false}
//      header="Example Modal"
//      buttons={buttons}
//      handleShow={this.handleLog.bind(this, 'Modal about to show', 'info')}
//      handleShown={this.handleLog.bind(this, 'Modal showing', 'success')}
//      handleHide={this.handleLog.bind(this, 'Modal about to hide', 'warning')}
//      handleHidden={this.handleLog.bind(this, 'Modal hidden', 'danger')}
//    >
//
I'm the content.

//
That's about it, really.

//    </ExampleModal>
//

//

// To show the modal, call this.refs.modal.show() from the parent component:
// handleShowModal: function() {
//   this.refs.modal.show();
// }

export default React.createClass({
      displayName: "VolumeAttachModal",

      mixins: [BootstrapModalMixin],

      propTypes: {
        volume: React.PropTypes.instanceOf(Backbone.Model).isRequired,
        project: React.PropTypes.instanceOf(Backbone.Model).isRequired,
        helpLink: React.PropTypes.instanceOf(Backbone.Model).isRequired
      },

      isSubmittable: function () {
        var hasInstanceId = !!this.state.instanceId;
        return hasInstanceId;
      },

      //
      // Mounting & State
      // ----------------
      //

      getState: function() {
        if (this.props.project === undefined)
            throw new Error("Volume attach modal lacks a project");

        var project = this.props.project;

        // TODO: remove ambiguity between state/this.state
        var state = {
            instances: stores.ProjectInstanceStore.getInstancesFor(project),
            instanceId: null
        };

        this.state = this.state || {};

        // Use selected instance or default to the first one
        if (state.instances) {
          var volume = this.props.volume,
            InstanceCollection = state.instances.constructor;

          // Filter out instances not in the same provider as the volume
          state.instances = state.instances.filter(function (i) {
            return i.get('identity').provider === volume.get('identity').provider;
          });
          state.instances = new InstanceCollection(state.instances);
          state.instanceId = this.state.instanceId || state.instances.first().id;
        }

        return state;
      },

      getInitialState: function () {
        return this.getState();
      },

      updateState: function () {
        if (this.isMounted()) this.setState(this.getState());
      },

      componentDidMount: function () {
        stores.InstanceStore.addChangeListener(this.updateState);
        stores.ProjectInstanceStore.addChangeListener(this.updateState);
      },

      componentWillUnmount: function () {
        stores.InstanceStore.removeChangeListener(this.updateState);
        stores.ProjectInstanceStore.removeChangeListener(this.updateState);
      },

      //
      // Internal Modal Callbacks
      // ------------------------
      //

      cancel: function () {
        this.hide();
      },

      confirm: function () {
        this.hide();
        var instance = this.state.instances.get(this.state.instanceId);
        if (instance === undefined) throw new Error("Instance not found in modal's instances store");
        this.props.onConfirm(instance);
      },

      //
      // Custom Modal Callbacks
      // ----------------------
      //

      onInstanceChange: function (e) {
        var newInstanceId = ~~e.target.value;
        this.setState({instanceId: newInstanceId});
      },

      //
      // Render
      // ------
      //

      renderLoadingContent: function () {
        return (

              {this.renderCloseButton()}
              Attach Volume

        )
      },

      renderAttachRulesContent: function () {
        return (

              Volume Attach Rules

                    Uh oh!
                    It looks like you don't have any instances in this project
                    that you can attach the volume to. Volumes can only be
                    attached to instances that are active, in
                    the same project and on the same
                    provider as the volume.

                    {
                      "If you'd like to attach this volume to an instance, you'll first need to "
                    }
                    create an instance
                    {
                      " on the same provider or move an existing instance into this project."
                    }

              <button type="button" className="btn btn-primary" onClick={this.cancel}>
                OK
              </button>

        )
      },

      renderAttachVolumeContent: function (instances) {
        var instanceId = this.state.instanceId;

        return (

              {this.renderCloseButton()}
              Attach Volume

Select the instance from the list below that you would like to attach the volume to:

                  Instance
                  <InstanceSelect
                    instanceId={instanceId}
                    instances={instances}
                    onChange={this.onInstanceChange}
                    />

              <button type="button" className="btn btn-danger" onClick={this.cancel}>
                Cancel
              </button>
              <button type="button" className="btn btn-primary" onClick={this.confirm} disabled={!this.isSubmittable()}>
                Attach volume to instance
              </button>

        )
      },

      render: function () {
        var instances = this.state.instances,
            content;

        var activeInstances = instances && instances.filter(i => i.is_active());

        if (instances == null) {
          content = this.renderLoadingContent();
        } else if (activeInstances.length > 0) {
          content = this.renderAttachVolumeContent(activeInstances);
        } else {
          content = this.renderAttachRulesContent();
        }

        return (

              {content}

        );
      }
});
<commit_msg>Renamed _next_ state variable for clarity<commit_after>import React from 'react';
import Backbone from 'backbone';
import BootstrapModalMixin from 'components/mixins/BootstrapModalMixin.react';
import stores from 'stores';
import InstanceSelect from '../volume_attach/InstanceSelect.react';

// Example Usage from http://bl.ocks.org/insin/raw/8449696/
// render: function(){
//

//   ...custom components...
//   <ExampleModal
//      ref="modal"
//      show={false}
//      header="Example Modal"
//      buttons={buttons}
//      handleShow={this.handleLog.bind(this, 'Modal about to show', 'info')}
//      handleShown={this.handleLog.bind(this, 'Modal showing', 'success')}
//      handleHide={this.handleLog.bind(this, 'Modal about to hide', 'warning')}
//      handleHidden={this.handleLog.bind(this, 'Modal hidden', 'danger')}
//    >
//
I'm the content.

//
That's about it, really.

//    </ExampleModal>
//

//

// To show the modal, call this.refs.modal.show() from the parent component:
// handleShowModal: function() {
//   this.refs.modal.show();
// }

export default React.createClass({
      displayName: "VolumeAttachModal",

      mixins: [BootstrapModalMixin],

      propTypes: {
        volume: React.PropTypes.instanceOf(Backbone.Model).isRequired,
        project: React.PropTypes.instanceOf(Backbone.Model).isRequired,
        helpLink: React.PropTypes.instanceOf(Backbone.Model).isRequired
      },

      isSubmittable: function () {
        var hasInstanceId = !!this.state.instanceId;
        return hasInstanceId;
      },

      //
      // Mounting & State
      // ----------------
      //

      getState: function() {
        if (this.props.project === undefined)
            throw new Error("Volume attach modal lacks a project");

        var project = this.props.project;

        // prepare to build out the `next` state for the component
        var next = {
            instances: stores.ProjectInstanceStore.getInstancesFor(project),
            instanceId: null
        };

        this.state = this.state || {};

        // Use selected instance or default to the first one
        if (next.instances) {
          var volume = this.props.volume,
            InstanceCollection = next.instances.constructor;

          // Filter out instances not in the same provider as the volume
          next.instances = next.instances.filter(function (i) {
            return i.get('identity').provider === volume.get('identity').provider;
          });
          next.instances = new InstanceCollection(next.instances);
          next.instanceId = this.state.instanceId || next.instances.first().id;
        }

        return next;
      },

      getInitialState: function () {
        return this.getState();
      },

      updateState: function () {
        if (this.isMounted()) this.setState(this.getState());
      },

      componentDidMount: function () {
        stores.InstanceStore.addChangeListener(this.updateState);
        stores.ProjectInstanceStore.addChangeListener(this.updateState);
      },

      componentWillUnmount: function () {
        stores.InstanceStore.removeChangeListener(this.updateState);
        stores.ProjectInstanceStore.removeChangeListener(this.updateState);
      },

      //
      // Internal Modal Callbacks
      // ------------------------
      //

      cancel: function () {
        this.hide();
      },

      confirm: function () {
        this.hide();
        var instance = this.state.instances.get(this.state.instanceId);
        if (instance === undefined) throw new Error("Instance not found in modal's instances store");
        this.props.onConfirm(instance);
      },

      //
      // Custom Modal Callbacks
      // ----------------------
      //

      onInstanceChange: function (e) {
        var newInstanceId = ~~e.target.value;
        this.setState({instanceId: newInstanceId});
      },

      //
      // Render
      // ------
      //

      renderLoadingContent: function () {
        return (

              {this.renderCloseButton()}
              Attach Volume

        )
      },

      renderAttachRulesContent: function () {
        return (

              Volume Attach Rules

                    Uh oh!
                    It looks like you don't have any instances in this project
                    that you can attach the volume to. Volumes can only be
                    attached to instances that are active, in
                    the same project and on the same
                    provider as the volume.

                    {
                      "If you'd like to attach this volume to an instance, you'll first need to "
                    }
                    create an instance
                    {
                      " on the same provider or move an existing instance into this project."
                    }

              <button type="button" className="btn btn-primary" onClick={this.cancel}>
                OK
              </button>

        )
      },

      renderAttachVolumeContent: function (instances) {
        var instanceId = this.state.instanceId;

        return (

              {this.renderCloseButton()}
              Attach Volume

Select the instance from the list below that you would like to attach the volume to:

                  Instance
                  <InstanceSelect
                    instanceId={instanceId}
                    instances={instances}
                    onChange={this.onInstanceChange}
                    />

              <button type="button" className="btn btn-danger" onClick={this.cancel}>
                Cancel
              </button>
              <button type="button" className="btn btn-primary" onClick={this.confirm} disabled={!this.isSubmittable()}>
                Attach volume to instance
              </button>

        )
      },

      render: function () {
        var instances = this.state.instances,
            content;

        var activeInstances = instances && instances.filter(i => i.is_active());

        if (instances == null) {
          content = this.renderLoadingContent();
        } else if (activeInstances.length > 0) {
          content = this.renderAttachVolumeContent(activeInstances);
        } else {
          content = this.renderAttachRulesContent();
        }

        return (

              {content}

        );
      }
});
<|endoftext|>