Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 0745b1ee1c94bcd89fd7c684bd13be5bb5093c53 0 parents
@jferris jferris authored
12 .gitignore
@@ -0,0 +1,12 @@
+*.swp
+bin
+*.swo
+*~
+*.o
+*.moc
+Makefile*
+qrc_*
+*.xcodeproj
+*.app
+moc_*.cpp
+.bundle
5 Gemfile
@@ -0,0 +1,5 @@
+source "http://rubygems.org"
+gem "rake"
+gem "rspec", :require => false
+gem "capybara"
+
50 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
61 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]
+
7 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
+
79 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
+
64 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
52 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
+
34 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><body>
+ <script type="text/javascript">
+ document.write("he" + "llo");
+ </script>
+ </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
+
10 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) }
+
18 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;
+}
+
28 src/Command.h
@@ -0,0 +1,28 @@
+#ifndef COMMAND_H
+#define COMMAND_H
+
+#include <QObject>
+
+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
+
74 src/Connection.cpp
@@ -0,0 +1,74 @@
+#include "Connection.h"
+#include "Visit.h"
+#include "Find.h"
+#include "Command.h"
+#include "Reset.h"
+
+#include <QTcpSocket>
+#include <iostream>
+
+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");
+ }
+}
+
25 src/Connection.h
@@ -0,0 +1,25 @@
+#include <QObject>
+
+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;
+};
+
57 src/Find.cpp
@@ -0,0 +1,57 @@
+#include "Find.h"
+#include "Command.h"
+#include "WebPage.h"
+#include <iostream>
+
+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);
+}
+
13 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);
+};
+
+
13 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("<html><body></body></html>");
+ QString response = "";
+ emit finished(true, response);
+}
+
12 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();
+};
+
23 src/Server.cpp
@@ -0,0 +1,23 @@
+#include "Server.h"
+#include "WebPage.h"
+#include "Connection.h"
+
+#include <QTcpServer>
+#include <iostream>
+
+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);
+}
+
20 src/Server.h
@@ -0,0 +1,20 @@
+#include <QObject>
+
+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;
+};
+
21 src/Visit.cpp
@@ -0,0 +1,21 @@
+#include "Visit.h"
+#include "Command.h"
+#include "WebPage.h"
+#include <iostream>
+
+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);
+}
+
16 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);
+};
+
+
9 src/WebPage.cpp
@@ -0,0 +1,9 @@
+#include "WebPage.h"
+
+WebPage::WebPage(QObject *parent) : QWebPage(parent) {
+}
+
+bool WebPage::shouldInterruptJavaScript() {
+ return false;
+}
+
13 src/WebPage.h
@@ -0,0 +1,13 @@
+#include <QtWebKit>
+#include <iostream>
+
+class WebPage : public QWebPage {
+ Q_OBJECT
+
+ public:
+ WebPage(QObject *parent = 0);
+
+ public slots:
+ bool shouldInterruptJavaScript();
+};
+
22 src/main.cpp
@@ -0,0 +1,22 @@
+#include "Server.h"
+#include <QtGui>
+
+#include <iostream>
+
+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;
+ }
+}
+
8 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
+
13 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);
+}
+
13 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);
+};
+
4 webkit_server.pro
@@ -0,0 +1,4 @@
+TEMPLATE = subdirs
+CONFIG += ordered
+SUBDIRS += src/webkit_server.pro
+
Please sign in to comment.
Something went wrong with that request. Please try again.