From 0745b1ee1c94bcd89fd7c684bd13be5bb5093c53 Mon Sep 17 00:00:00 2001 From: Joe Ferris Date: Fri, 18 Feb 2011 22:53:06 -0500 Subject: [PATCH] Initial commit --- .gitignore | 12 ++++ Gemfile | 5 ++ Gemfile.lock | 50 +++++++++++++++++ Rakefile | 61 +++++++++++++++++++++ lib/capybara-webkit.rb | 7 +++ lib/capybara/driver/webkit.rb | 79 +++++++++++++++++++++++++++ lib/capybara/driver/webkit/browser.rb | 64 ++++++++++++++++++++++ lib/capybara/driver/webkit/node.rb | 52 ++++++++++++++++++ spec/driver_spec.rb | 34 ++++++++++++ spec/spec_helper.rb | 10 ++++ src/Command.cpp | 18 ++++++ src/Command.h | 28 ++++++++++ src/Connection.cpp | 74 +++++++++++++++++++++++++ src/Connection.h | 25 +++++++++ src/Find.cpp | 57 +++++++++++++++++++ src/Find.h | 13 +++++ src/Reset.cpp | 13 +++++ src/Reset.h | 12 ++++ src/Server.cpp | 23 ++++++++ src/Server.h | 20 +++++++ src/Visit.cpp | 21 +++++++ src/Visit.h | 16 ++++++ src/WebPage.cpp | 9 +++ src/WebPage.h | 13 +++++ src/main.cpp | 22 ++++++++ src/webkit_server.pro | 8 +++ templates/Command.cpp | 13 +++++ templates/Command.h | 13 +++++ webkit_server.pro | 4 ++ 29 files changed, 776 insertions(+) create mode 100644 .gitignore create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 Rakefile create mode 100644 lib/capybara-webkit.rb create mode 100644 lib/capybara/driver/webkit.rb create mode 100644 lib/capybara/driver/webkit/browser.rb create mode 100644 lib/capybara/driver/webkit/node.rb create mode 100644 spec/driver_spec.rb create mode 100644 spec/spec_helper.rb create mode 100644 src/Command.cpp create mode 100644 src/Command.h create mode 100644 src/Connection.cpp create mode 100644 src/Connection.h create mode 100644 src/Find.cpp create mode 100644 src/Find.h create mode 100644 src/Reset.cpp create mode 100644 src/Reset.h create mode 100644 src/Server.cpp create mode 100644 src/Server.h create mode 100644 src/Visit.cpp create mode 100644 src/Visit.h create mode 100644 src/WebPage.cpp create mode 100644 src/WebPage.h create mode 100644 src/main.cpp create mode 100644 src/webkit_server.pro create mode 100644 templates/Command.cpp create mode 100644 templates/Command.h create mode 100644 webkit_server.pro diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cfe6d74 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.swp +bin +*.swo +*~ +*.o +*.moc +Makefile* +qrc_* +*.xcodeproj +*.app +moc_*.cpp +.bundle diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..a9eb52d --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +source "http://rubygems.org" +gem "rake" +gem "rspec", :require => false +gem "capybara" + diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..750d356 --- /dev/null +++ b/Gemfile.lock @@ -0,0 +1,50 @@ +GEM + remote: http://rubygems.org/ + specs: + capybara (0.4.1.2) + celerity (>= 0.7.9) + culerity (>= 0.2.4) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + selenium-webdriver (>= 0.0.27) + xpath (~> 0.1.3) + celerity (0.8.8) + childprocess (0.1.7) + ffi (~> 0.6.3) + culerity (0.2.15) + diff-lcs (1.1.2) + ffi (0.6.3) + rake (>= 0.8.7) + json_pure (1.5.1) + mime-types (1.16) + nokogiri (1.4.4) + rack (1.2.1) + rack-test (0.5.7) + rack (>= 1.0) + rake (0.8.7) + rspec (2.5.0) + rspec-core (~> 2.5.0) + rspec-expectations (~> 2.5.0) + rspec-mocks (~> 2.5.0) + rspec-core (2.5.1) + rspec-expectations (2.5.0) + diff-lcs (~> 1.1.2) + rspec-mocks (2.5.0) + rubyzip (0.9.4) + selenium-webdriver (0.1.3) + childprocess (~> 0.1.5) + ffi (~> 0.6.3) + json_pure + rubyzip + xpath (0.1.3) + nokogiri (~> 1.3) + +PLATFORMS + ruby + +DEPENDENCIES + capybara + rake + rspec diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..7b43126 --- /dev/null +++ b/Rakefile @@ -0,0 +1,61 @@ +require 'rubygems' +require 'bundler/setup' +require 'fileutils' +require 'rspec/core/rake_task' + +desc "Generate a new command called NAME" +task :generate_command do + name = ENV['NAME'] or raise "Provide a name with NAME=" + + header = "src/#{name}.h" + source = "src/#{name}.cpp" + + %w(h cpp).each do |extension| + File.open("templates/Command.#{extension}", "r") do |source_file| + contents = source_file.read + contents.gsub!("NAME", name) + File.open("src/#{name}.#{extension}", "w") do |target_file| + target_file.write(contents) + end + end + end + + Dir.glob("src/*.pro").each do |project_file_name| + project = IO.read(project_file_name) + project.gsub!(/(HEADERS = .*)/, "\\1 #{name}.h") + project.gsub!(/(SOURCES = .*)/, "\\1 #{name}.cpp") + File.open(project_file_name, "w") { |file| file.write(project) } + end +end + +desc "Generate a Makefile using qmake" +file 'Makefile' do + sh("qmake -spec macx-g++") +end + +desc "Regenerate dependencies using qmake" +task :qmake => 'Makefile' do + sh("make qmake") +end + +desc "Build the webkit server" +task :build => :qmake do + sh("make") + + FileUtils.mkdir("bin") unless File.directory?("bin") + + if File.exist?("src/webkit_server.app") + FileUtils.cp("src/webkit_server.app/Contents/MacOS/webkit_server", "bin") + else + FileUtils.cp("src/webkit_server", "bin") + end +end + +RSpec::Core::RakeTask.new do |t| + t.pattern = "spec/*_spec.rb" + t.rspec_opts = "--format progress" +end + +desc "Default: build and run all specs" +task :default => [:build, :spec] + diff --git a/lib/capybara-webkit.rb b/lib/capybara-webkit.rb new file mode 100644 index 0000000..b7cfde1 --- /dev/null +++ b/lib/capybara-webkit.rb @@ -0,0 +1,7 @@ +require "capybara" +require "capybara/driver/webkit" + +Capybara.register_driver :webkit do |app| + Capybara::Driver::Webkit.new(app) +end + diff --git a/lib/capybara/driver/webkit.rb b/lib/capybara/driver/webkit.rb new file mode 100644 index 0000000..abdcd43 --- /dev/null +++ b/lib/capybara/driver/webkit.rb @@ -0,0 +1,79 @@ +require "capybara" +require "capybara/driver/webkit/node" +require "capybara/driver/webkit/browser" + +class Capybara::Driver::Webkit + def initialize(app, options={}) + @app = app + @options = options + @rack_server = Capybara::Server.new(@app) + @rack_server.boot if Capybara.run_server + @browser = Browser.new + end + + def current_url + raise NotImplementedError + end + + def visit(path) + @browser.visit(url(path)) + end + + def find(query) + @browser.find(query).map { |native| Node.new(self, native) } + end + + def source + raise NotImplementedError + end + + def body + raise NotImplementedError + end + + def execute_script(script) + raise Capybara::NotSupportedByDriverError + end + + def evaluate_script(script) + raise Capybara::NotSupportedByDriverError + end + + def response_headers + raise Capybara::NotSupportedByDriverError + end + + def status_code + raise Capybara::NotSupportedByDriverError + end + + def within_frame(frame_id) + raise Capybara::NotSupportedByDriverError + end + + def within_window(handle) + raise Capybara::NotSupportedByDriverError + end + + def wait? + false + end + + def wait_until(*args) + end + + def reset! + @browser.reset! + end + + def has_shortcircuit_timeout? + false + end + + private + + def url(path) + @rack_server.url(path) + end +end + diff --git a/lib/capybara/driver/webkit/browser.rb b/lib/capybara/driver/webkit/browser.rb new file mode 100644 index 0000000..e079996 --- /dev/null +++ b/lib/capybara/driver/webkit/browser.rb @@ -0,0 +1,64 @@ +require 'socket' + +class Capybara::Driver::Webkit + class Browser + def initialize + start_server + connect + end + + def visit(url) + command "visit", url + end + + def find(query) + command("find", query).split(",") + end + + def reset! + command("reset") + end + + private + + def start_server + @pid = fork { exec("webkit_server") } + at_exit { Process.kill("INT", @pid) } + end + + def connect + puts ">> Connecting" + Capybara.timeout(5) do + attempt_connect + !@socket.nil? + end + puts ">> Connected" + end + + def attempt_connect + @socket = TCPSocket.open("localhost", 9200) + rescue Errno::ECONNREFUSED + end + + def check + result = @socket.gets.strip + puts ">> #{result}" + unless result == 'ok' + raise + end + end + + def command(name, *args) + puts ">> Sending #{name}" + @socket.puts name + args.each { |arg| @socket.puts arg } + check + read_response + end + + def read_response + response_length = @socket.gets.to_i + @socket.read(response_length) + end + end +end diff --git a/lib/capybara/driver/webkit/node.rb b/lib/capybara/driver/webkit/node.rb new file mode 100644 index 0000000..28ea410 --- /dev/null +++ b/lib/capybara/driver/webkit/node.rb @@ -0,0 +1,52 @@ +class Capybara::Driver::Webkit + class Node < Capybara::Driver::Node + def text + raise NotImplementedError + end + + def [](name) + raise NotImplementedError + end + + def value + raise NotImplementedError + end + + def set(value) + raise NotImplementedError + end + + def select_option + raise NotImplementedError + end + + def unselect_option + raise NotImplementedError + end + + def click + raise NotImplementedError + end + + def drag_to(element) + raise NotImplementedError + end + + def tag_name + raise NotImplementedError + end + + def visible? + raise NotImplementedError + end + + def path + raise NotSupportedByDriverError + end + + def trigger(event) + raise NotSupportedByDriverError + end + end +end + diff --git a/spec/driver_spec.rb b/spec/driver_spec.rb new file mode 100644 index 0000000..4aaa346 --- /dev/null +++ b/spec/driver_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' +require 'capybara/driver/webkit' + +describe Capybara::Driver::Webkit do + let(:hello_app) do + lambda do |env| + body = <<-HTML + + + + HTML + [200, + { 'Content-Type' => 'text/html', 'Content-Length' => body.length.to_s }, + [body]] + end + end + + subject { Capybara::Driver::Webkit.new(hello_app) } + after { subject.reset! } + + it "finds content after loading a URL" do + subject.visit("/hello") + subject.find("//*[contains(., 'hello')]").should_not be_empty + end + + it "has an empty page after reseting" do + subject.visit("/") + subject.reset! + subject.find("//*[contains(., 'hello')]").should be_empty + end +end + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..2d1628a --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,10 @@ +require 'rspec' +require 'rspec/autorun' + +PROJECT_ROOT = File.expand_path(File.join(File.dirname(__FILE__), '..')).freeze + +$LOAD_PATH << File.join(PROJECT_ROOT, 'lib') +ENV["PATH"] = ENV["PATH"] + ":" + File.join(PROJECT_ROOT, "bin") + +Dir[File.join(PROJECT_ROOT, 'spec', 'support', '**', '*.rb')].each { |file| require(file) } + diff --git a/src/Command.cpp b/src/Command.cpp new file mode 100644 index 0000000..7852849 --- /dev/null +++ b/src/Command.cpp @@ -0,0 +1,18 @@ +#include "Command.h" +#include "WebPage.h" + +Command::Command(WebPage *page, QObject *parent) : QObject(parent) { + m_page = page; +} + +void Command::receivedArgument(const char *argument) { + Q_UNUSED(argument); +} + +void Command::start() { +} + +WebPage *Command::page() { + return m_page; +} + diff --git a/src/Command.h b/src/Command.h new file mode 100644 index 0000000..3df167d --- /dev/null +++ b/src/Command.h @@ -0,0 +1,28 @@ +#ifndef COMMAND_H +#define COMMAND_H + +#include + +class WebPage; + +class Command : public QObject { + Q_OBJECT + + public: + Command(WebPage *page, QObject *parent = 0); + virtual void start(); + virtual void receivedArgument(const char *argument); + + signals: + void finished(bool success, QString &response); + + protected: + WebPage *page(); + + private: + WebPage *m_page; + +}; + +#endif + diff --git a/src/Connection.cpp b/src/Connection.cpp new file mode 100644 index 0000000..690f680 --- /dev/null +++ b/src/Connection.cpp @@ -0,0 +1,74 @@ +#include "Connection.h" +#include "Visit.h" +#include "Find.h" +#include "Command.h" +#include "Reset.h" + +#include +#include + +Connection::Connection(QTcpSocket *socket, WebPage *page, QObject *parent) : + QObject(parent) { + m_socket = socket; + m_page = page; + m_command = NULL; + connect(m_socket, SIGNAL(readyRead()), this, SLOT(checkNext())); +} + +void Connection::checkNext() { + std::cout << "<< Data ready to read" << std::endl; + while (m_socket->canReadLine()) { + readNext(); + } +} + +void Connection::readNext() { + std::cout << "<< Reading line" << std::endl; + char buffer[1024]; + qint64 lineLength = m_socket->readLine(buffer, 1024); + if (lineLength != -1) { + buffer[lineLength - 1] = 0; + std::cout << "<< Got line: " << buffer << std::endl; + if (m_command) { + m_command->receivedArgument(buffer); + } else { + m_command = startCommand(buffer); + if (m_command) { + connect(m_command, + SIGNAL(finished(bool, QString &)), + this, + SLOT(finishCommand(bool, QString &))); + m_command->start(); + } else { + m_socket->write("bad command\n"); + } + } + } +} + +Command *Connection::startCommand(const char *name) { + if (strcmp(name, "visit") == 0) { + return new Visit(m_page, this); + } else if (strcmp(name, "find") == 0) { + return new Find(m_page, this); + } else if (strcmp(name, "reset") == 0) { + return new Reset(m_page, this); + } else { + std::cout << ">> Unknown command" << std::endl; + return NULL; + } +} + +void Connection::finishCommand(bool success, QString &response) { + m_command->deleteLater(); + m_command = NULL; + if (success) { + m_socket->write("ok\n"); + QString responseLength = QString::number(response.size()) + "\n"; + m_socket->write(responseLength.toAscii()); + m_socket->write(response.toAscii()); + } else { + m_socket->write("failure\n"); + } +} + diff --git a/src/Connection.h b/src/Connection.h new file mode 100644 index 0000000..a0c6285 --- /dev/null +++ b/src/Connection.h @@ -0,0 +1,25 @@ +#include + +class QTcpSocket; +class WebPage; +class Command; + +class Connection : public QObject { + Q_OBJECT + + public: + Connection(QTcpSocket *socket, WebPage *page, QObject *parent = 0); + + public slots: + void checkNext(); + void finishCommand(bool success, QString &response); + + private: + void readNext(); + Command *startCommand(const char *name); + + QTcpSocket *m_socket; + Command *m_command; + WebPage *m_page; +}; + diff --git a/src/Find.cpp b/src/Find.cpp new file mode 100644 index 0000000..0ca52c8 --- /dev/null +++ b/src/Find.cpp @@ -0,0 +1,57 @@ +#include "Find.h" +#include "Command.h" +#include "WebPage.h" +#include + +Find::Find(WebPage *page, QObject *parent) : Command(page, parent) { +} + +void Find::receivedArgument(const char *xpath) { + std::cout << "<< Running query: " << xpath << std::endl; + QString javascript = QString("\ + (function () {\ + if (!window.__capybara_index) {\ + window.__capybara_index = 0;\ + window.__capybara_nodes = {};\ + }\ + var iterator = document.evaluate(\"") + xpath + "\",\ + document,\ + null,\ + XPathResult.ORDERED_NODE_ITERATOR_TYPE,\ + null);\ + var node;\ + var results = [];\ + while (node = iterator.iterateNext()) {\ + window.__capybara_index++;\ + window.__capybara_nodes[window.__capybara_index] = node;\ + results.push(window.__capybara_index);\ + }\ + return results;\ + })()\ + "; + + + std::cout << "<< Javascript to execute:" << std::endl; + std::cout << javascript.toAscii().data() << std::endl; + + QVariant result = page()->mainFrame()->evaluateJavaScript(javascript); + + QVariantList nodes = result.toList(); + QString response; + bool addComma = false; + + double node; + for (int i = 0; i < nodes.size(); i++) { + node = nodes[i].toDouble(); + if (addComma) + response.append(","); + response.append(QString::number(node)); + addComma = true; + } + + std::cout << "<< Got result:" << std::endl; + std::cout << response.toAscii().data() << std::endl; + + emit finished(true, response); +} + diff --git a/src/Find.h b/src/Find.h new file mode 100644 index 0000000..a8b15ae --- /dev/null +++ b/src/Find.h @@ -0,0 +1,13 @@ +#include "Command.h" + +class WebPage; + +class Find : public Command { + Q_OBJECT + + public: + Find(WebPage *page, QObject *parent = 0); + virtual void receivedArgument(const char *argument); +}; + + diff --git a/src/Reset.cpp b/src/Reset.cpp new file mode 100644 index 0000000..319c54a --- /dev/null +++ b/src/Reset.cpp @@ -0,0 +1,13 @@ +#include "Reset.h" +#include "WebPage.h" + +Reset::Reset(WebPage *page, QObject *parent) : Command(page, parent) { +} + +void Reset::start() { + page()->triggerAction(QWebPage::Stop); + page()->mainFrame()->setHtml(""); + QString response = ""; + emit finished(true, response); +} + diff --git a/src/Reset.h b/src/Reset.h new file mode 100644 index 0000000..905161b --- /dev/null +++ b/src/Reset.h @@ -0,0 +1,12 @@ +#include "Command.h" + +class WebPage; + +class Reset : public Command { + Q_OBJECT + + public: + Reset(WebPage *page, QObject *parent = 0); + virtual void start(); +}; + diff --git a/src/Server.cpp b/src/Server.cpp new file mode 100644 index 0000000..2cb81d2 --- /dev/null +++ b/src/Server.cpp @@ -0,0 +1,23 @@ +#include "Server.h" +#include "WebPage.h" +#include "Connection.h" + +#include +#include + +Server::Server(QObject *parent) : QObject(parent) { + m_tcp_server = new QTcpServer(this); + m_page = new WebPage(this); +} + +bool Server::start() { + connect(m_tcp_server, SIGNAL(newConnection()), this, SLOT(handleConnection())); + return m_tcp_server->listen(QHostAddress::Any, 9200); +} + +void Server::handleConnection() { + std::cout << "<< Got connection" << std::endl; + QTcpSocket *socket = m_tcp_server->nextPendingConnection(); + new Connection(socket, m_page, this); +} + diff --git a/src/Server.h b/src/Server.h new file mode 100644 index 0000000..fadccee --- /dev/null +++ b/src/Server.h @@ -0,0 +1,20 @@ +#include + +class QTcpServer; +class WebPage; + +class Server : public QObject { + Q_OBJECT + + public: + Server(QObject *parent = 0); + bool start(); + + public slots: + void handleConnection(); + + private: + QTcpServer *m_tcp_server; + WebPage *m_page; +}; + diff --git a/src/Visit.cpp b/src/Visit.cpp new file mode 100644 index 0000000..dbac86d --- /dev/null +++ b/src/Visit.cpp @@ -0,0 +1,21 @@ +#include "Visit.h" +#include "Command.h" +#include "WebPage.h" +#include + +Visit::Visit(WebPage *page, QObject *parent) : Command(page, parent) { + connect(page, SIGNAL(loadFinished(bool)), this, SLOT(loadFinished(bool))); +} + +void Visit::receivedArgument(const char *url) { + std::cout << ">> Loading page: " << url << std::endl; + page()->mainFrame()->setUrl(QUrl(url)); +} + +void Visit::loadFinished(bool success) { + std::cout << ">> Page loaded" << std::endl; + QString response; + std::cout << page()->mainFrame()->toHtml().toAscii().constData() << std::endl; + emit finished(success, response); +} + diff --git a/src/Visit.h b/src/Visit.h new file mode 100644 index 0000000..cd49795 --- /dev/null +++ b/src/Visit.h @@ -0,0 +1,16 @@ +#include "Command.h" + +class WebPage; + +class Visit : public Command { + Q_OBJECT + + public: + Visit(WebPage *page, QObject *parent = 0); + virtual void receivedArgument(const char *argument); + + private slots: + void loadFinished(bool success); +}; + + diff --git a/src/WebPage.cpp b/src/WebPage.cpp new file mode 100644 index 0000000..b9254cd --- /dev/null +++ b/src/WebPage.cpp @@ -0,0 +1,9 @@ +#include "WebPage.h" + +WebPage::WebPage(QObject *parent) : QWebPage(parent) { +} + +bool WebPage::shouldInterruptJavaScript() { + return false; +} + diff --git a/src/WebPage.h b/src/WebPage.h new file mode 100644 index 0000000..27c3501 --- /dev/null +++ b/src/WebPage.h @@ -0,0 +1,13 @@ +#include +#include + +class WebPage : public QWebPage { + Q_OBJECT + + public: + WebPage(QObject *parent = 0); + + public slots: + bool shouldInterruptJavaScript(); +}; + diff --git a/src/main.cpp b/src/main.cpp new file mode 100644 index 0000000..72db042 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,22 @@ +#include "Server.h" +#include + +#include + +int main(int argc, char **argv) { + QApplication app(argc, argv); + app.setApplicationName("akephalos-webkit"); + app.setOrganizationName("thoughtbot, inc"); + app.setOrganizationDomain("thoughtbot.com"); + + Server server; + if (server.start()) { + std::cout << "<< Started server" << std::endl; + + return app.exec(); + } else { + std::cerr << "Couldn't start server" << std::endl; + return 1; + } +} + diff --git a/src/webkit_server.pro b/src/webkit_server.pro new file mode 100644 index 0000000..eb21536 --- /dev/null +++ b/src/webkit_server.pro @@ -0,0 +1,8 @@ +TEMPLATE = app +TARGET = webkit_server +DESTDIR = . +HEADERS = WebPage.h Server.h Connection.h Command.h Visit.h Find.h Reset.h +SOURCES = main.cpp WebPage.cpp Server.cpp Connection.cpp Command.cpp Visit.cpp Find.cpp Reset.cpp +QT += network webkit +CONFIG += console staticlib + diff --git a/templates/Command.cpp b/templates/Command.cpp new file mode 100644 index 0000000..ec64e71 --- /dev/null +++ b/templates/Command.cpp @@ -0,0 +1,13 @@ +#include "NAME.h" +#include "WebPage.h" + +NAME::NAME(WebPage *page, QObject *parent) : Command(page, parent) { +} + +void NAME::start() { +} + +void NAME::receivedArgument(const char *argument) { + Q_UNUSED(argument); +} + diff --git a/templates/Command.h b/templates/Command.h new file mode 100644 index 0000000..e8dae92 --- /dev/null +++ b/templates/Command.h @@ -0,0 +1,13 @@ +#include "Command.h" + +class WebPage; + +class NAME : public Command { + Q_OBJECT + + public: + NAME(WebPage *page, QObject *parent = 0); + virtual void start(); + virtual void receivedArgument(const char *argument); +}; + diff --git a/webkit_server.pro b/webkit_server.pro new file mode 100644 index 0000000..a53442a --- /dev/null +++ b/webkit_server.pro @@ -0,0 +1,4 @@ +TEMPLATE = subdirs +CONFIG += ordered +SUBDIRS += src/webkit_server.pro +