Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Service worker support #391

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion lib/ferrum/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,12 @@ def create_target

def add_target(params)
target = Target.new(@browser, params)
if target.window?
if target.window? || target.worker?
@targets.put_if_absent(target.id, target)
else
@pendings.put(target, @browser.timeout)
end
target
end

def update_target(target_id, params)
Expand Down
19 changes: 15 additions & 4 deletions lib/ferrum/contexts.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,30 @@ def size

private

TARGET_TYPES = %w[page worker].freeze

def subscribe
@browser.client.on("Target.targetCreated") do |params|
info = params["targetInfo"]
next unless info["type"] == "page"
type = info["type"]
next unless TARGET_TYPES.include?(type)

target_id = info["targetId"]
@browser.command(
"Target.autoAttachRelated",
targetId: target_id,
waitForDebuggerOnStart: true, # Needed to capture all network requests
filter: [{type: "worker"}])

context_id = info["browserContextId"]
@contexts[context_id]&.add_target(info)
target = @contexts[context_id]&.add_target(info)
target.build if type == "worker"
end

@browser.client.on("Target.targetInfoChanged") do |params|
info = params["targetInfo"]
next unless info["type"] == "page"

next unless TARGET_TYPES.include?(info["type"])
q
context_id, target_id = info.values_at("browserContextId", "targetId")
@contexts[context_id]&.update_target(target_id, info)
end
Expand Down
23 changes: 17 additions & 6 deletions lib/ferrum/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,12 @@ def reset
# @return [Cookies]
attr_reader :cookies

def initialize(target_id, browser, proxy: nil)
def initialize(target_id, browser, proxy: nil, type: "page")
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want me to use Page or create another class for workers?

@frames = Concurrent::Map.new
@main_frame = Frame.new(nil, self)
@browser = browser
@target_id = target_id
@type = type
@timeout = @browser.timeout
@event = Event.new.tap(&:set)
self.proxy = proxy
Expand Down Expand Up @@ -355,13 +356,21 @@ def subscribe
end

def prepare_page
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I keep everything in this method or split out the worker preparation into a different method?

command("Page.enable")
command("Runtime.enable")
command("DOM.enable")
command("CSS.enable")
command("Log.enable")
if @type == "page"
command("Page.enable")
command("DOM.enable")
command("CSS.enable")
command("Runtime.enable")
command("Log.enable")
command("ServiceWorker.enable")
end

command("Network.enable")

if @type == "worker"
command("Runtime.runIfWaitingForDebugger")
end

if use_authorized_proxy?
network.authorize(user: @proxy_user,
password: @proxy_password,
Expand All @@ -387,6 +396,8 @@ def prepare_page

inject_extensions

return if @type == "worker"

width, height = @browser.window_size
resize(width: width, height: height)

Expand Down
32 changes: 29 additions & 3 deletions lib/ferrum/target.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class Target
# You can create page yourself and assign it to target, used in cuprite
# where we enhance page class and build page ourselves.
attr_writer :page
attr_reader :connection

def initialize(browser, params = nil)
@page = nil
Expand All @@ -23,12 +24,19 @@ def attached?
end

def page
@page ||= build_page
connection if page?
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially I had this return nil if it was a worker because other parts of the code were causing failures otherwise, but not sure what the impact of this is.

end

def network
connection.network
end

def build(**options)
connection(**options)
end

def build_page(**options)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I just alias_method this to build?

maybe_sleep_if_new_window
Page.new(id, @browser, **options)
connection(**options)
end

def id
Expand Down Expand Up @@ -59,9 +67,27 @@ def window?
!!opener_id
end

def page?
@params["type"] == "page"
end

def worker?
@params["type"] == "worker"
end

def maybe_sleep_if_new_window
# Dirty hack because new window doesn't have events at all
sleep(NEW_WINDOW_WAIT) if window?
end

def connection(**options)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what the proper naming would be for this if it can be both a page and a worker.

@connection ||= begin
maybe_sleep_if_new_window if page?

options.merge!(type: @params["type"])

Page.new(id, @browser, **options)
end
end
end
end
13 changes: 13 additions & 0 deletions spec/network_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
browser.go_to("/ferrum/with_js")
expect(browser.network.traffic.length).to eq(4)
end

it "keeps track of service workers" do
page.go_to("/ferrum/service_worker")

browser.network.wait_for_idle
traffic = browser.targets.values.map { _1.network.traffic }.flatten
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the worker bubble up its traffic to its parent page?

urls = traffic.map { |e| e.request.url }

expect(urls.size).to eq(3)
expect(urls.grep(%r{/ferrum/service_worker$}).size).to eq(1)
expect(urls.grep(%r{/ferrum/one.png$}).size).to eq(1)
expect(urls.grep(%r{^blob:}).size).to eq(1)
end
end

it "#wait_for_idle" do
Expand Down
5 changes: 5 additions & 0 deletions spec/support/application.rb
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,11 @@ def authorized?(login, password)
File.read("#{FERRUM_PUBLIC}/jquery-ui-1.11.4.min.js")
end

get "/ferrum/one.png" do
content_type :png
File.read("#{FERRUM_PUBLIC}/one.png")
end

get "/ferrum/unexist.png" do
halt 404
end
Expand Down
Binary file added spec/support/public/one.png
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just an image I came up with on Figma, no particular attachment to it if you'd like something else. I tried a SVG but it didn't work with the createImageBitmap code.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions spec/support/views/service_worker.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<html>
<body>
<div id="service_worker_code" style="display: none">
async function imageFromUrl(url) {
const response = await fetch(url);

if (!response.ok) {
throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
}

const blobResult = await response.blob();
const imageBitmap = await createImageBitmap(blobResult);
return imageBitmap;
}
self.onmessage = async (event) => {
try {
const image = await imageFromUrl(event.data.url);

self.postMessage({
data: image
}, [image]);
}
catch(e)
{
self.postMessage({
error: e
});
}
};
</div>
<script>
var code = document.getElementById("service_worker_code").innerText;
var objectUrl = URL.createObjectURL(new Blob([code],{ type: "application/javascript"}))
var worker = new Worker(objectUrl);

worker.addEventListener("message", function(event) {
let canvas = document.getElementById('canvas');
let context = canvas.getContext('2d');
let imageBitmap = event.data.data;
context.drawImage(imageBitmap, 0, 0);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Drawn to the canvas so the page isn't blank.

});

let path = "one.png"
let url = new URL(path, document.baseURI).href;
worker.postMessage({ url: url });
</script>
<canvas width="400" height="400" id="canvas"></canvas>
</body>
</html>