diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9106b2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/.bundle/ +/.yardoc +/_yardoc/ +/coverage/ +/doc/ +/pkg/ +/spec/reports/ +/tmp/ diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..0e40fe8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ + +# Default ignored files +/workspace.xml \ No newline at end of file diff --git a/.idea/.rakeTasks b/.idea/.rakeTasks new file mode 100644 index 0000000..26cf061 --- /dev/null +++ b/.idea/.rakeTasks @@ -0,0 +1,7 @@ + + diff --git a/.idea/kinchan.iml b/.idea/kinchan.iml new file mode 100644 index 0000000..30b1925 --- /dev/null +++ b/.idea/kinchan.iml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8b5ddd5 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..9662f9b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.rakeTasks b/.rakeTasks new file mode 100644 index 0000000..26cf061 --- /dev/null +++ b/.rakeTasks @@ -0,0 +1,7 @@ + + diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..6c69e1c --- /dev/null +++ b/Gemfile @@ -0,0 +1,6 @@ +source "https://rubygems.org" + +# Specify your gem's dependencies in kinchan.gemspec +gemspec + +gem "rake", "~> 12.0" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..4229a8b --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,27 @@ +PATH + remote: . + specs: + kinchan (0.1.0) + require_all (~> 3.0.0) + selenium-webdriver (~> 3.142.6) + +GEM + remote: https://rubygems.org/ + specs: + childprocess (3.0.0) + rake (12.3.2) + require_all (3.0.0) + rubyzip (2.0.0) + selenium-webdriver (3.142.6) + childprocess (>= 0.5, < 4.0) + rubyzip (>= 1.2.2) + +PLATFORMS + ruby + +DEPENDENCIES + kinchan! + rake (~> 12.0) + +BUNDLED WITH + 2.1.2 diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..5d70c39 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Kyle McGough + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..295b2cc --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# [Kinchan](https://itazuranakiss.fandom.com/wiki/Kinnosuke_Ikezawa) +## Composable browser automation for Ruby. + +### Requirements + +* Ruby +* Bundler + +### Getting Started + +Create a new directory for your Kinchan project + +`mkdir kinchan_project && cd kinchan_project` + +Create the tasks directory where you'll store your Kinchan tasks + +`mkdir tasks` + +Initialize bundler + +`bundle init` + +Add the Kinchan gem to your bundle + +`bundle add kinchan` + +Now that we've got the structure the rest of the readme will teach you how to create, compose, and run Kinchan tasks 👽 + +### Creating a Task + +In your tasks directory create a Ruby file with any name + +e.g. `touch ruby_reddit.rb` + +In your task's Ruby file you'll need to `require 'kinchan'` + +and then define your task as a class that inherits from `Kinchan::Task` + +so far your file should look a little something like this +```ruby +require 'kinchan' + +class VisitRubysReddit < Kinchan::Task +end +``` + +All Kinchan tasks require an execute method that takes a single parameter (the selenium browser object) like so + +```ruby +require 'kinchan' + +class VisitRubyReddit < Kinchan::Task + def execute(browser) + browser.navigate.to 'https://old.reddit.com/r/ruby' + end +end +``` + +That's all it takes to create a basic task! If we run it we'll see a browser process start and navigate to the Ruby subreddit. For a full description of the +browser API check out the [wiki page](https://github.com/SeleniumHQ/selenium/wiki/Ruby-Bindings) for Ruby Selenium (they call it a "driver"). + +### Running a Task + +Create a Ruby file in the root level of your Kinchan project and require your task + +Create a new instance of the task and call `run` + +e.g. + +```ruby +require_relative 'tasks/ruby_reddit' + +VisitRubyReddit.new.run +``` + +That's all it takes to run a task. Kinchan handles the rest. + +### Passing Data to a Task + +A task's initialize function can accept options, just don't forget to call super + +e.g. + +```ruby +class Search < Kinchan::Task + def initialize(**options) + super + @query = options[:query] + end + + def execute(browser) + browser.navigate.to 'https://www.google.com/search?q=#{CGI.escape(@query)}' + end +end +``` + +### Composing Tasks + +Tasks can call any number of other tasks either before or after they execute, and their dependencies +will have their dependencies ran and so on + +This is done by specifying dependencies with a task's `@before_tasks` or `@after_tasks` in their initialize method + +e.g. + +```ruby +class PrintFirstResult < Kinchan::Task + def initialize(**options) + super + @before_tasks << { task: :search, options: options } + # specify that the search task should run, with the same options, before running this task + end + + def execute(browser) + puts browser.execute_script "return document.querySelector('.srg a').innerText" + end +end +``` + +Task's do not need to be in the same scope, as long as the task exists Kinchan will find and run it when appropriate + +### Setting Selenium Browser Options + +before running your task you can modify the selenium browser options like so + +```ruby +Kinchan::Task.browser = :chrome +Kinchan::Task.browser_options = Selenium::WebDriver::Chrome::Options.new +Kinchan::Task.browser_options.add_argument('--headless') +``` diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..43022f7 --- /dev/null +++ b/Rakefile @@ -0,0 +1,2 @@ +require "bundler/gem_tasks" +task :default => :spec diff --git a/bin/console b/bin/console new file mode 100755 index 0000000..41a68d0 --- /dev/null +++ b/bin/console @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require "bundler/setup" +require "kinchan" + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require "irb" +IRB.start(__FILE__) diff --git a/bin/setup b/bin/setup new file mode 100755 index 0000000..dce67d8 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here diff --git a/kinchan.gemspec b/kinchan.gemspec new file mode 100644 index 0000000..a7e6b9a --- /dev/null +++ b/kinchan.gemspec @@ -0,0 +1,30 @@ +require_relative 'lib/kinchan/version' + +Gem::Specification.new do |spec| + spec.name = "kinchan" + spec.version = Kinchan::VERSION + spec.authors = ["Kyle McGough"] + spec.email = ["contact@squared.technology"] + + spec.summary = %q{Composable browser automation with Ruby.} + spec.description = %q{Composable browser automation with Ruby. Create, compose, and run tasks that automate the browser with Selenium.} + spec.homepage = "https://github.com/sosodev/kinchan" + spec.license = "MIT" + spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = spec.homepage + spec.metadata["changelog_uri"] = spec.homepage + + # Specify which files should be added to the gem when it is released. + # The `git ls-files -z` loads the files in the RubyGem that have been added into git. + spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do + `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } + end + spec.bindir = "exe" + spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } + spec.require_paths = ["lib"] + + spec.add_runtime_dependency 'selenium-webdriver', ['~> 3.142.6'] + spec.add_runtime_dependency 'require_all', ['~> 3.0.0'] +end diff --git a/lib/kinchan.rb b/lib/kinchan.rb new file mode 100644 index 0000000..e427ae5 --- /dev/null +++ b/lib/kinchan.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require 'kinchan/version' +require 'selenium-webdriver' +require 'require_all' + +module Kinchan + class Error < StandardError; end + + class Task + singleton_class.send(:attr_accessor, :browser) + singleton_class.send(:attr_accessor, :browser_options) + singleton_class.send(:attr_reader, :descendants) + @descendants = [] + @browser = :chrome + @browser_options = nil + @@browser_webdriver = nil + + def initialize(**options) + @before_tasks = [] + @after_tasks = [] + @options = options + end + + def self.inherited(subclass) + Task.descendants << subclass + end + + def self.find_task(task_symbol) + Task.descendants.select { |task| task.name.split('::').last.downcase == task_symbol.to_s.downcase }[0] + end + + def self.restart_browser + unless @@browser_webdriver.nil? + @@browser_webdriver.close + @@browser_webdriver = Selenium::WebDriver.for Task.browser + end + end + + def execute(browser); end + + def run + if @@browser_webdriver.nil? + if Task.browser_options.nil? + @@browser_webdriver = Selenium::WebDriver.for Task.browser + else + @@browser_webdriver = Selenium::WebDriver.for(Task.browser, options: Task.browser_options) + end + end + + @before_tasks.each do |task_hash| + task = Task.find_task(task_hash[:task]) + task.new(**task_hash[:options]).public_send('run') unless task.nil? + end + + execute(@@browser_webdriver) + + @after_tasks.each do |task_hash| + task = Task.find_task(task_hash[:task]) + task.new(**task_hash[:options]).public_send('run') unless task.nil? + end + end + end +end + +require_all 'tasks' diff --git a/lib/kinchan/version.rb b/lib/kinchan/version.rb new file mode 100644 index 0000000..4908d19 --- /dev/null +++ b/lib/kinchan/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Kinchan + VERSION = '0.1.0' +end