diff --git a/lib/puppeteer/frame.rb b/lib/puppeteer/frame.rb
index c51b777a..419b31e5 100644
--- a/lib/puppeteer/frame.rb
+++ b/lib/puppeteer/frame.rb
@@ -266,9 +266,7 @@ def title
# @param frame_payload [Hash]
def navigated(frame_payload)
@name = frame_payload['name']
- # TODO(lushnikov): remove this once requestInterception has loaderId exposed.
- @navigation_url = frame_payload['url']
- @url = frame_payload['url']
+ @url = "#{frame_payload['url']}#{frame_payload['urlFragment']}"
# Ensure loaderId updated.
# The order of [Page.lifecycleEvent name="init"] and [Page.frameNavigated] is random... for some reason...
diff --git a/spec/integration/frame_spec.rb b/spec/integration/frame_spec.rb
new file mode 100644
index 00000000..9d686a84
--- /dev/null
+++ b/spec/integration/frame_spec.rb
@@ -0,0 +1,334 @@
+require 'spec_helper'
+
+RSpec.describe Puppeteer::Frame do
+ context 'with empty page' do
+ sinatra do
+ get('/') do
+ '
Hello puppeteer!'
+ end
+ end
+
+ describe '#execution_context' do
+ include Utils::AttachFrame
+
+ it 'should work' do
+ page.goto('http://127.0.0.1:4567/')
+ attach_frame(page, 'frame1', '/')
+ expect(page.frames.size).to eq(2)
+
+ frames = page.frames
+ contexts = frames.map(&:execution_context)
+ expect(contexts).to all(be_truthy)
+ expect(contexts.first).not_to eq(contexts.last)
+ expect(contexts.map(&:frame)).to eq(frames)
+
+ contexts.each_with_index do |context, i|
+ context.evaluate("() => (globalThis.a = #{i + 1})")
+ end
+ values = contexts.map { |context| context.evaluate('() => globalThis.a') }
+ expect(values).to eq([1, 2])
+ end
+ end
+
+ describe '#evaluate_handle' do
+ it 'should work' do
+ page.goto('http://127.0.0.1:4567/')
+ main_frame = page.main_frame
+ window_handle = main_frame.evaluate_handle('() => window')
+ expect(window_handle).to be_truthy
+ end
+ end
+
+ describe '#evaluate' do
+ include Utils::AttachFrame
+ include Utils::DetachFrame
+
+ it 'should throw for detached frames' do
+ page.goto('http://127.0.0.1:4567/')
+ frame1 = attach_frame(page, 'frame1', '/')
+ detach_frame(page, 'frame1')
+ expect {
+ frame1.evaluate('() => 7 * 8')
+ }.to raise_error(/Execution Context is not available in detached frame/)
+ end
+ end
+ end
+
+ context 'with nested frames page' do
+ sinatra do
+ get('/nested-frames.html') do
+ <<~HTML
+
+
+
+
+ HTML
+ end
+ get('/two-frames.html') do
+ <<~HTML
+
+
+
+ HTML
+ end
+ get('/frame.html') do
+ <<~HTML
+
+
+ Hi, I'm frame
+ HTML
+ end
+ end
+
+ include Utils::DumpFrames
+
+ it 'should handle nested frames' do
+ page.goto('http://127.0.0.1:4567/nested-frames.html')
+ expect(dump_frames(page.main_frame)).to eq([
+ 'http://127.0.0.1:/nested-frames.html',
+ ' http://127.0.0.1:/two-frames.html (2frames)',
+ ' http://127.0.0.1:/frame.html (uno)',
+ ' http://127.0.0.1:/frame.html (dos)',
+ ' http://127.0.0.1:/frame.html (aframe)',
+ ])
+ end
+ end
+
+ # itFailsFirefox(
+ # 'should send events when frames are manipulated dynamically',
+ # async () => {
+ # const { page, server } = getTestState();
+
+ # await page.goto(server.EMPTY_PAGE);
+ # // validate frameattached events
+ # const attachedFrames = [];
+ # page.on('frameattached', (frame) => attachedFrames.push(frame));
+ # await utils.attachFrame(page, 'frame1', './assets/frame.html');
+ # expect(attachedFrames.length).toBe(1);
+ # expect(attachedFrames[0].url()).toContain('/assets/frame.html');
+
+ # // validate framenavigated events
+ # const navigatedFrames = [];
+ # page.on('framenavigated', (frame) => navigatedFrames.push(frame));
+ # await utils.navigateFrame(page, 'frame1', './empty.html');
+ # expect(navigatedFrames.length).toBe(1);
+ # expect(navigatedFrames[0].url()).toBe(server.EMPTY_PAGE);
+
+ # // validate framedetached events
+ # const detachedFrames = [];
+ # page.on('framedetached', (frame) => detachedFrames.push(frame));
+ # await utils.detachFrame(page, 'frame1');
+ # expect(detachedFrames.length).toBe(1);
+ # expect(detachedFrames[0].isDetached()).toBe(true);
+ # }
+ # );
+ # itFailsFirefox(
+ # 'should send "framenavigated" when navigating on anchor URLs',
+ # async () => {
+ # const { page, server } = getTestState();
+
+ # await page.goto(server.EMPTY_PAGE);
+ # await Promise.all([
+ # page.goto(server.EMPTY_PAGE + '#foo'),
+ # utils.waitEvent(page, 'framenavigated'),
+ # ]);
+ # expect(page.url()).toBe(server.EMPTY_PAGE + '#foo');
+ # }
+ # );
+ # it('should persist mainFrame on cross-process navigation', async () => {
+ # const { page, server } = getTestState();
+
+ # await page.goto(server.EMPTY_PAGE);
+ # const mainFrame = page.mainFrame();
+ # await page.goto(server.CROSS_PROCESS_PREFIX + '/empty.html');
+ # expect(page.mainFrame() === mainFrame).toBeTruthy();
+ # });
+ # it('should not send attach/detach events for main frame', async () => {
+ # const { page, server } = getTestState();
+
+ # let hasEvents = false;
+ # page.on('frameattached', () => (hasEvents = true));
+ # page.on('framedetached', () => (hasEvents = true));
+ # await page.goto(server.EMPTY_PAGE);
+ # expect(hasEvents).toBe(false);
+ # });
+ # itFailsFirefox('should detach child frames on navigation', async () => {
+ # const { page, server } = getTestState();
+
+ # let attachedFrames = [];
+ # let detachedFrames = [];
+ # let navigatedFrames = [];
+ # page.on('frameattached', (frame) => attachedFrames.push(frame));
+ # page.on('framedetached', (frame) => detachedFrames.push(frame));
+ # page.on('framenavigated', (frame) => navigatedFrames.push(frame));
+ # await page.goto(server.PREFIX + '/frames/nested-frames.html');
+ # expect(attachedFrames.length).toBe(4);
+ # expect(detachedFrames.length).toBe(0);
+ # expect(navigatedFrames.length).toBe(5);
+
+ # attachedFrames = [];
+ # detachedFrames = [];
+ # navigatedFrames = [];
+ # await page.goto(server.EMPTY_PAGE);
+ # expect(attachedFrames.length).toBe(0);
+ # expect(detachedFrames.length).toBe(4);
+ # expect(navigatedFrames.length).toBe(1);
+ # });
+ # itFailsFirefox('should support framesets', async () => {
+ # const { page, server } = getTestState();
+
+ # let attachedFrames = [];
+ # let detachedFrames = [];
+ # let navigatedFrames = [];
+ # page.on('frameattached', (frame) => attachedFrames.push(frame));
+ # page.on('framedetached', (frame) => detachedFrames.push(frame));
+ # page.on('framenavigated', (frame) => navigatedFrames.push(frame));
+ # await page.goto(server.PREFIX + '/frames/frameset.html');
+ # expect(attachedFrames.length).toBe(4);
+ # expect(detachedFrames.length).toBe(0);
+ # expect(navigatedFrames.length).toBe(5);
+
+ # attachedFrames = [];
+ # detachedFrames = [];
+ # navigatedFrames = [];
+ # await page.goto(server.EMPTY_PAGE);
+ # expect(attachedFrames.length).toBe(0);
+ # expect(detachedFrames.length).toBe(4);
+ # expect(navigatedFrames.length).toBe(1);
+ # });
+ # itFailsFirefox('should report frame from-inside shadow DOM', async () => {
+ # const { page, server } = getTestState();
+
+ # await page.goto(server.PREFIX + '/shadow.html');
+ # await page.evaluate(async (url: string) => {
+ # const frame = document.createElement('iframe');
+ # frame.src = url;
+ # document.body.shadowRoot.appendChild(frame);
+ # await new Promise((x) => (frame.onload = x));
+ # }, server.EMPTY_PAGE);
+ # expect(page.frames().length).toBe(2);
+ # expect(page.frames()[1].url()).toBe(server.EMPTY_PAGE);
+ # });
+
+ context 'with empty page' do
+ include Utils::AttachFrame
+
+ sinatra do
+ get('/') do
+ 'Hello puppeteer!'
+ end
+ end
+
+ before { page.goto('http://127.0.0.1:4567/') }
+
+ it 'should report frame.name()' do
+ attach_frame(page, 'theFrameId', '/')
+ js = <<~JAVASCRIPT
+ function (url) {
+ const frame = document.createElement('iframe');
+ frame.name = 'theFrameName';
+ frame.src = url;
+ document.body.appendChild(frame);
+ return new Promise((x) => (frame.onload = x));
+ }
+ JAVASCRIPT
+ page.evaluate(js, '/')
+ expect(page.frames.map(&:name)).to eq(['', 'theFrameId', 'theFrameName'])
+ end
+
+ it 'should report frame.parent()' do
+ attach_frame(page, 'frame1', '/')
+ attach_frame(page, 'frame2', '/')
+ expect(page.frames.map(&:parent_frame)).to eq([nil, page.main_frame, page.main_frame])
+ end
+
+ it 'should report different frame instance when frame re-attaches' do
+ frame1 = attach_frame(page, 'frame1', '/')
+ js = <<~JAVASCRIPT
+ () => {
+ globalThis.frame = document.querySelector('#frame1');
+ globalThis.frame.remove();
+ }
+ JAVASCRIPT
+ page.evaluate(js)
+ expect(frame1).to be_detached
+
+ frame2 = await_all(
+ resolvable_future { |f| page.once('frameattached') { |frame| f.fulfill(frame) }},
+ page.async_evaluate('() => document.body.appendChild(globalThis.frame)'),
+ ).first
+ expect(frame2).not_to be_detached
+ expect(frame1).not_to eq(frame2)
+ end
+ end
+
+ context 'with one-frame-url-fragment page' do
+ sinatra do
+ get('/one-frame-url-fragment.html') do
+ ""
+ end
+ get('/frame.html') do
+ <<~HTML
+
+
+
+ Hi, I'm frame
+ HTML
+ end
+ end
+
+ it 'should support url fragment' do
+ page.goto('http://127.0.0.1:4567/one-frame-url-fragment.html')
+
+ expect(page.frames.size).to eq(2)
+ expect(page.frames.last.url).to eq('http://127.0.0.1:4567/frame.html?param=value#fragment')
+ end
+ end
+end
diff --git a/spec/utils.rb b/spec/utils.rb
index 9357cb7f..e961a11a 100644
--- a/spec/utils.rb
+++ b/spec/utils.rb
@@ -15,3 +15,27 @@ def attach_frame(page, frame_id, url)
page.evaluate_handle(js, frame_id, url).as_element.content_frame
end
end
+
+module Utils::DetachFrame
+ def detach_frame(page, frame_id)
+ js = <<~JAVASCRIPT
+ function detachFrame(frameId) {
+ const frame = document.getElementById(frameId);
+ frame.remove();
+ }
+ JAVASCRIPT
+ page.evaluate(js, frame_id)
+ end
+end
+
+module Utils::DumpFrames
+ def dump_frames(frame, indentation = '')
+ description = frame.url.gsub(/:\d{4}\//, ':/')
+ if frame.name && frame.name.length > 0
+ description = "#{description} (#{frame.name})"
+ end
+ ["#{indentation}#{description}"] + frame.child_frames.flat_map do |child|
+ dump_frames(child, " #{indentation}")
+ end
+ end
+end