Initial commit

This commit is contained in:
Joe Ferris 2011-02-18 22:53:06 -05:00
commit 0745b1ee1c
29 changed files with 776 additions and 0 deletions

12
.gitignore vendored Normal file
View File

@ -0,0 +1,12 @@
*.swp
bin
*.swo
*~
*.o
*.moc
Makefile*
qrc_*
*.xcodeproj
*.app
moc_*.cpp
.bundle

5
Gemfile Normal file
View File

@ -0,0 +1,5 @@
source "http://rubygems.org"
gem "rake"
gem "rspec", :require => false
gem "capybara"

50
Gemfile.lock Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -0,0 +1,7 @@
require "capybara"
require "capybara/driver/webkit"
Capybara.register_driver :webkit do |app|
Capybara::Driver::Webkit.new(app)
end

View File

@ -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

View File

@ -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

View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -0,0 +1,9 @@
#include "WebPage.h"
WebPage::WebPage(QObject *parent) : QWebPage(parent) {
}
bool WebPage::shouldInterruptJavaScript() {
return false;
}

13
src/WebPage.h Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -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 Normal file
View File

@ -0,0 +1,4 @@
TEMPLATE = subdirs
CONFIG += ordered
SUBDIRS += src/webkit_server.pro