# Directive Examples

In this section, we look at few examples of custom directives one can write.

# The most basic Directive!

Lets start by writing the most basic directive.

class Directive extends VirtuosoDirective {
  // no code here!
}

This directive simply inherits the core Virtuoso directive that guides the exploration. You can write the same directive in another way:

class Directive extends VirtuosoDirective {
  exploreFromCheckpoint() {
      // simply return what the Virtuoso directive would return by default
      return super.exploreFromCheckpoint()
  }
}

What does the default directive do?

By default, Virtuoso's default directive clicks on interactable elements (e.g., links, buttons, etc.), and fills forms it can find on the page; just like a normal user would.

# Clicking on buttons

class Directive extends VirtuosoDirective {
    exploreFromCheckpoint() {
        return this.getState().elements.buttons.map(button => ({
            instructions: [
                this.instruction.click(button)
            ]
        }))
    }
}
class Directive extends VirtuosoDirective {
    exploreFromCheckpoint() {
        const stateElements = this.getState().elements
        return [...stateElements.links, ...stateElements.buttons].map(element => ({
            instructions: [
                this.instruction.click(element)
            ]
        }))
    }
}

# Writing in input fields

class Directive extends VirtuosoDirective {
    exploreFromCheckpoint() {
        // write a random word in every input field
        return this.getState().elements.inputs.map(input => ({
            instructions: [
                this.instruction.write(input, this.getRandom('ipsum'))
            ]
        }))
    }
}

# Fill forms automatically

class Directive extends VirtuosoDirective {
    exploreFromCheckpoint() {
        // randomly fill every form on the page
        return this.getState().elements.forms.flatMap(e => this.instruction.fillForm(e))
    }
}
class Directive extends VirtuosoDirective {
    exploreFromCheckpoint() {
        return this.getState().elements.links.map(link => ({
            instructions: [
                this.instruction.assert({
                    type: 'ASSERT_EQUALS',
                    element: { selectors: [{ type: 'XPATH', value: '//title' }] },
                    value: this.getState().title
                }),
                this.instruction.click(link)
            ]
        }))
    }
}

# Execute some rules against your pages

class Directive extends VirtuosoDirective {
    exploreFromCheckpoint() {
        var scriptInstructions = this.getScripts()
            // this is an example of getting a list of scripts that match a criteria
            .filter(script => script.name === 'YOUR SCRIPT NAME')
            .map(s => virtuoso.instruction.execute(s))
            .map(instruction => virtuoso.instruction.makeContinueOnError(instruction));

        /*
         * The following clicks on every link, waits for 2 seconds, and then executes your scripts
         */
        return this.getState().elements.links.map(e => ({
            instructions: [
                virtuoso.instruction.click(e),
                virtuoso.instruction.wait(2000), // optionally wait for page to load
                ...scriptInstructions // run all scripts / rules
            ]
        }))
    }
}

# Avoid exploring beyond the boundaries of your own website

Sometimes you may be linking to pages that you do not wish to explore further from (e.g., your Linkedin, Twitter or Instagram page). Virtuoso provides a native function shouldExploreCheckpoint() (which you can also override) that helps you filter URLs that do not match the same subdomain of your page.

class Directive extends VirtuosoDirective {
    exploreFromCheckpoint() {
        // shouldExploreCheckpoint() is a feature of directives that checks if the domain of the discovered checkpoint is different from your own site
        if (!this.shouldExploreCheckpoint()) return []
        return this.getState().links.buttons.map(link => ({instructions: [this.instruction.click(link)]}))
    }
}

Debugging

Given that directive tooling is still at an experimental stage, debugging is currently limited to error messages produced inside exploration jobs. If a directive fails or throws an error, you can expand the job on the dashboard, and see the reason for the failure, which can contain the JavaScript execution trace. Dedicated tooling for debugging will be available in future releases.

# Skip interactions with elements that appear on multiple pages

The following directive uses KV storage to avoid repeated interaction with elements that appear on multiple pages.

function hashString(str) {
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    hash += Math.pow(str.charCodeAt(i) * 31, str.length - i);
    hash = hash & hash; // Convert to 32bit integer
  }
  return hash + "";
}

function hashElement(element) {
  if (!element) return ''
  if (element.url) return hashString(element.url)
  if (element.attributes?.href) return hashString(element.attributes.href)
  if (element.text && element.text.trim()) return hashString(element.text.trim())
  return hashString(JSON.stringify(element.selectors))
}

function hashInteraction(interaction) {
  return interaction.instructions
    // would look like: CLICK:123,NAVIGATE:,WRITE:433
    .map(i => `${i.command}:${hashElement(i.instructionCommand?.target)}`).join(',')
}

class Directive extends VirtuosoDirective {
  exploreFromCheckpoint() {
    // these are proposed interactions that virtuoso would have done by default
    const interactions = super.exploreFromCheckpoint()

    // if virtuoso was not going to make any interactions, we can return here
    if (interactions.length === 0) return interactions

    const seenInteractions = this.getSeenInteractions()

    const newInteractions = []

    for (const interaction of interactions) {
      const signature = hashInteraction(interaction)
      if (!seenInteractions.has(signature)) {
        newInteractions.push(interaction)
        seenInteractions.add(signature)
      }
    }

    this.putSeenInteractions(seenInteractions)

    return newInteractions
  }

  /**
   * @returns {Set} set of interaction hashes
   */
  getSeenInteractions() {
    const seenString = this.kvGetString('seen')
    if (!seenString) return new Set()
    return new Set(JSON.parse(seenString))
  }

  putSeenInteractions(interactionsSet) {
    this.kvPutString('seen', JSON.stringify([...interactionsSet]))
  }
}
Last Updated: 3/29/2021, 8:58:45 AM