blob: f55a037c94c431505d69a4c27b3576994f7f0820 [file] [log] [blame]
# Copyright 2019 The Cobalt Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Connect to the web debugger and run some commands."""
# pylint: disable=g-doc-args,g-doc-return-or-yield,g-doc-exception
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import json
import logging
import os
from six.moves.urllib.parse import urlsplit
from six.moves.urllib.parse import urlunsplit
import sys
from cobalt.black_box_tests import black_box_tests
from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
from starboard.tools import config
sys.path.append(
os.path.join(
os.path.dirname(__file__), '..', '..', '..', 'third_party',
'websocket-client'))
import websocket # pylint: disable=wrong-import-position
# Set to True to add additional logging to debug the test.
_DEBUG = False
class DebuggerCommandError(Exception):
"""Exception when an error response is received for a command."""
def __init__(self, error):
code = f"[{error['code']}] " if 'code' in error else ''
super().__init__(code + error['message'])
class DebuggerEventError(Exception):
"""Exception when an unexpected event is received."""
def __init__(self, expected, actual):
super().__init__(f'Waiting for {expected} but got {actual}')
class JavaScriptError(Exception):
"""Exception when a JavaScript exception occurs in an evaluation."""
def __init__(self, exception_details):
# All the fields we care about are optional, so gracefully fallback.
ex = exception_details.get('exception', {})
fallback = ex.get('className', 'Unknown error') + ' (No description)'
msg = ex.get('description', fallback)
super().__init__(msg)
class DebuggerConnection(object):
"""Connection to debugger over a WebSocket.
Runs in the owner's thread by receiving and storing messages as needed when
waiting for a command response or event.
"""
def __init__(self, ws_url, timeout=1):
self.ws_url = ws_url
self.timeout = timeout
self.last_id = 0
self.commands = {}
self.responses = {}
self.events = []
def __enter__(self):
websocket.enableTrace(_DEBUG)
logging.debug('Debugger WebSocket: %s', self.ws_url)
self.ws = websocket.create_connection(self.ws_url, timeout=self.timeout)
return self
def __exit__(self, exc_type, exc_value, exc_traceback):
self.ws.close()
def run_command(self, method, params=None):
"""Runs a debugger command and waits for the response.
Fails the test with an exception if the debugger backend returns an error
response, or the command times out without receiving a response.
"""
response = self.wait_response(self.send_command(method, params))
if 'error' in response:
raise DebuggerCommandError(response['error'])
return response
def send_command(self, method, params=None):
"""Sends an async debugger command and returns the command id."""
self.last_id += 1
msg = {
'id': self.last_id,
'method': method,
}
if params:
msg['params'] = params
if _DEBUG:
logging.debug('send >>>>>>>>\n%s\n>>>>>>>>',
json.dumps(msg, sort_keys=True, indent=4))
logging.debug('send command: %s', method)
self.ws.send(json.dumps(msg))
self.commands[self.last_id] = msg
return self.last_id
def wait_response(self, command_id):
"""Wait for the response for a command to arrive.
Fails the test with an exception if the WebSocket times out without
receiving the response.
"""
assert command_id in self.commands
# Receive messages until the response we want arrives.
# It may have already arrived before we even enter the loop.
while command_id not in self.responses:
try:
self._receive_message()
except websocket.WebSocketTimeoutException as e:
method = self.commands[command_id]['method']
raise DebuggerCommandError(
{'message': 'Timeout waiting for response to ' + method}) from e
self.commands.pop(command_id)
return self.responses.pop(command_id)
def wait_event(self, method):
"""Wait for an event to arrive.
Fails the test with an exception if the WebSocket times out without
receiving the event, or another event arrives first.
"""
# "Debugger.scriptParsed" events get sent as artifacts of the debugger
# backend implementation running its own injected code, so they are ignored
# unless that's what we're waiting for.
allow_script_parsed = method == 'Debugger.scriptParsed'
# Pop already-received events from the event queue.
def _next_event():
while self.events:
event = self.events.pop(0)
if allow_script_parsed or event['method'] != 'Debugger.scriptParsed':
return event
return None
# Take the next event in the queue. If the queue is empty, receive messages
# until an event arrives and is put in the queue for us to take.
while True:
event = _next_event()
if event:
break
try:
self._receive_message()
except websocket.WebSocketTimeoutException as e:
raise DebuggerEventError(method, 'None (timeout)') from e
if method != event['method']:
raise DebuggerEventError(method, event['method'])
return event
def _receive_message(self):
"""Receives one message and stores it in either responses or events."""
msg = json.loads(self.ws.recv())
if _DEBUG:
logging.debug('recv <<<<<<<<\n%s\n<<<<<<<<',
json.dumps(msg, sort_keys=True, indent=4))
if 'id' in msg:
self.responses[msg['id']] = msg
elif 'method' in msg:
logging.debug('receive event: %s', msg['method'])
self.events.append(msg)
else:
raise ValueError('Unexpected debugger message', msg)
def enable_runtime(self):
"""Helper to enable the 'Runtime' domain and save the context_id."""
self.run_command('Runtime.enable')
context_event = self.wait_event('Runtime.executionContextCreated')
self.context_id = context_event['params']['context']['id']
assert self.context_id > 0
def enable_dom(self):
"""Helper to enable the 'DOM' domain and get the document."""
self.run_command('DOM.enable')
self.wait_event('DOM.documentUpdated')
doc_response = self.run_command('DOM.getDocument')
return doc_response['result']['root']
def evaluate_js(self, expression):
"""Helper for the 'Runtime.evaluate' command to run some JavaScript."""
if _DEBUG:
logging.debug('JavaScript eval -------- %s', expression)
response = self.run_command('Runtime.evaluate', {
'contextId': self.context_id,
'expression': expression,
})
if 'exceptionDetails' in response['result']:
raise JavaScriptError(response['result']['exceptionDetails'])
return response['result']
class WebDebuggerTest(black_box_tests.BlackBoxTestCase):
"""Test interaction with the web debugger over a WebSocket."""
def set_up_with(self, cm):
val = cm.__enter__() # pylint: disable=unnecessary-dunder-call
self.addCleanup(cm.__exit__, None, None, None)
return val
def setUp(self):
is_gold_config = self.launcher_params.config == config.Config.GOLD
if is_gold_config:
self.skipTest('DevTools is disabled on this platform')
self.server = self.set_up_with(
ThreadedWebServer(binding_address=self.GetBindingAddress()))
url = self.server.GetURL(file_name='testdata/web_debugger.html')
self.runner = self.set_up_with(self.CreateCobaltRunner(url=url))
self.debugger = self.set_up_with(self.create_debugger_connection())
self.runner.WaitForJSTestsSetup()
self.debugger.enable_runtime()
def tearDown(self):
self.debugger.evaluate_js('onEndTest()')
self.assertTrue(self.runner.JSTestsSucceeded())
unprocessed_events = [
e['method']
for e in self.debugger.events
if e['method'] != 'Debugger.scriptParsed'
]
self.assertEqual([], unprocessed_events)
def create_debugger_connection(self):
devtools_url = self.runner.GetCval('Cobalt.Server.DevTools')
parts = list(urlsplit(devtools_url))
parts[0] = 'ws' # scheme
parts[2] = '/devtools/page/cobalt' # path
ws_url = urlunsplit(parts)
return DebuggerConnection(ws_url)
def test_runtime(self):
# Evaluate a simple expression.
eval_result = self.debugger.evaluate_js('6 * 7')
self.assertEqual(42, eval_result['result']['value'])
# Set an attribute and read it back w/ WebDriver.
self.debugger.evaluate_js(
'document.body.setAttribute("web_debugger", "tested")')
self.assertEqual(
'tested',
self.runner.UniqueFind('body').get_attribute('web_debugger'))
# Log to console, and check we get the console event.
self.debugger.evaluate_js('console.log("hello")')
console_event = self.debugger.wait_event('Runtime.consoleAPICalled')
self.assertEqual('hello', console_event['params']['args'][0]['value'])
def test_dom_tree(self):
doc_root = self.debugger.enable_dom()
self.assertEqual('#document', doc_root['nodeName'])
doc_url = doc_root['documentURL']
# remove query params (cert_scope, etc.)
doc_url = doc_url.split('?')[0]
self.assertEqual(self.runner.url, doc_url)
# document: <html><head></head><body></body></html>
html_node = doc_root['children'][0]
body_node = html_node['children'][1]
self.assertEqual('BODY', body_node['nodeName'])
# <body>
# <h1><span>Web debugger</span></h1>
# <div#test>
# <div#A><div#A1/><div#A2/></div#A>
# <div#B><span#B1/></div#B>
# <span#C/>
# </div#test>
# <script/>
# </body>
# Request children of BODY including the entire subtree.
self.debugger.run_command(
'DOM.requestChildNodes',
{
'nodeId': body_node['nodeId'],
'depth': -1, # entire subtree
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
self.assertEqual(3, len(child_nodes_event['params']['nodes']))
h1 = child_nodes_event['params']['nodes'][0]
span_h1 = h1['children'][0]
text_h1 = span_h1['children'][0]
self.assertEqual(1, len(h1['children']))
self.assertEqual(1, len(span_h1['children']))
self.assertNotIn('children', text_h1)
self.assertEqual(1, h1['childNodeCount'])
self.assertEqual(1, span_h1['childNodeCount']) # non-whitespace text
self.assertEqual(0, text_h1['childNodeCount'])
self.assertEqual('H1', h1['nodeName'])
self.assertEqual('SPAN', span_h1['nodeName'])
self.assertEqual('#text', text_h1['nodeName'])
self.assertEqual('Web debugger', text_h1['nodeValue'])
div_test = child_nodes_event['params']['nodes'][1]
div_a = div_test['children'][0]
div_a1 = div_a['children'][0]
div_a2 = div_a['children'][1]
div_b = div_test['children'][1]
span_b1 = div_b['children'][0]
text_b1 = span_b1['children'][0] # 1 deeper than requested
span_c = div_test['children'][2]
text_c = span_c['children'][0]
self.assertEqual(3, len(div_test['children']))
self.assertEqual(2, len(div_a['children']))
self.assertNotIn('children', div_a1)
self.assertNotIn('children', div_a2)
self.assertEqual(1, len(div_b['children']))
self.assertEqual(1, len(span_b1['children']))
self.assertNotIn('children', text_b1)
self.assertEqual(1, len(span_c['children']))
self.assertNotIn('children', text_c)
self.assertEqual(3, div_test['childNodeCount'])
self.assertEqual(2, div_a['childNodeCount'])
self.assertEqual(0, div_a1['childNodeCount'])
self.assertEqual(0, div_a2['childNodeCount'])
self.assertEqual(1, div_b['childNodeCount'])
self.assertEqual(0, span_b1['childNodeCount']) # whitespace text
self.assertEqual(0, text_b1['childNodeCount'])
self.assertEqual(0, span_c['childNodeCount']) # whitespace text
self.assertEqual(0, text_c['childNodeCount'])
self.assertEqual(['id', 'test'], div_test['attributes'])
self.assertEqual(['id', 'A'], div_a['attributes'])
self.assertEqual(['id', 'A1'], div_a1['attributes'])
self.assertEqual(['id', 'A2'], div_a2['attributes'])
self.assertEqual(['id', 'B'], div_b['attributes'])
self.assertEqual(['id', 'B1'], span_b1['attributes'])
self.assertEqual(['id', 'C'], span_c['attributes'])
self.assertEqual(3, text_b1['nodeType']) # Text
self.assertEqual('', text_b1['nodeValue'].strip())
self.assertEqual(3, text_c['nodeType']) # Text
self.assertEqual('', text_c['nodeValue'].strip())
# Request children of BODY to depth 2.
# Not reporting children of <div#A> & <div#B>.
# Reporting lone text child of <span#C>
self.debugger.run_command('DOM.requestChildNodes', {
'nodeId': body_node['nodeId'],
'depth': 2,
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
self.assertEqual(3, len(child_nodes_event['params']['nodes']))
h1 = child_nodes_event['params']['nodes'][0]
span_h1 = h1['children'][0]
text_h1 = span_h1['children'][0] # 1 deeper than requested
self.assertEqual('H1', h1['nodeName'])
self.assertEqual('SPAN', span_h1['nodeName'])
self.assertEqual('#text', text_h1['nodeName'])
self.assertEqual('Web debugger', text_h1['nodeValue'])
div_test = child_nodes_event['params']['nodes'][1]
div_a = div_test['children'][0]
div_b = div_test['children'][1]
span_c = div_test['children'][2]
text_c = span_c['children'][0] # 1 deeper than requested
self.assertEqual(3, len(div_test['children']))
self.assertNotIn('children', div_a)
self.assertNotIn('children', div_b)
self.assertEqual(1, len(span_c['children']))
self.assertNotIn('children', text_c)
self.assertEqual(3, div_test['childNodeCount'])
self.assertEqual(2, div_a['childNodeCount'])
self.assertEqual(1, div_b['childNodeCount'])
self.assertEqual(0, span_c['childNodeCount']) # whitespace text
self.assertEqual(['id', 'test'], div_test['attributes'])
self.assertEqual(['id', 'A'], div_a['attributes'])
self.assertEqual(['id', 'B'], div_b['attributes'])
self.assertEqual(['id', 'C'], span_c['attributes'])
self.assertEqual(3, text_c['nodeType']) # Text
self.assertEqual('', text_c['nodeValue'].strip())
# Request children of BODY to default depth of 1.
# Not reporting children of <div#test>
self.debugger.run_command('DOM.requestChildNodes', {
'nodeId': body_node['nodeId'],
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
self.assertEqual(3, len(child_nodes_event['params']['nodes']))
h1 = child_nodes_event['params']['nodes'][0]
div_test = child_nodes_event['params']['nodes'][1]
self.assertNotIn('children', h1)
self.assertNotIn('children', div_test)
self.assertEqual(1, h1['childNodeCount'])
self.assertEqual(3, div_test['childNodeCount'])
self.assertEqual(['id', 'test'], div_test['attributes'])
def test_dom_remote_object(self):
doc_root = self.debugger.enable_dom()
html_node = doc_root['children'][0]
body_node = html_node['children'][1]
self.debugger.run_command('DOM.requestChildNodes', {
'nodeId': body_node['nodeId'],
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
div_test = child_nodes_event['params']['nodes'][1]
# Get <div#A1> as a remote object, and request it as a node.
# This sends 'DOM.setChildNodes' events for unknown nodes on the path from
# <div#test>, which is the nearest ancestor that was already reported.
eval_result = self.debugger.evaluate_js('document.getElementById("A1")')
node_response = self.debugger.run_command('DOM.requestNode', {
'objectId': eval_result['result']['objectId'],
})
# Event reporting the children of <div#test>
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
self.assertEqual(div_test['nodeId'],
child_nodes_event['params']['parentId'])
div_a = child_nodes_event['params']['nodes'][0]
div_b = child_nodes_event['params']['nodes'][1]
self.assertEqual(['id', 'A'], div_a['attributes'])
self.assertEqual(['id', 'B'], div_b['attributes'])
# Event reporting the children of <div#A>
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
self.assertEqual(div_a['nodeId'], child_nodes_event['params']['parentId'])
div_a1 = child_nodes_event['params']['nodes'][0]
div_a2 = child_nodes_event['params']['nodes'][1]
self.assertEqual(['id', 'A1'], div_a1['attributes'])
self.assertEqual(['id', 'A2'], div_a2['attributes'])
# The node we requested now has an ID as reported the last event.
self.assertEqual(div_a1['nodeId'], node_response['result']['nodeId'])
# Round trip resolving test div to an object, then back to a node.
resolve_response = self.debugger.run_command('DOM.resolveNode', {
'nodeId': div_test['nodeId'],
})
node_response = self.debugger.run_command('DOM.requestNode', {
'objectId': resolve_response['result']['object']['objectId'],
})
self.assertEqual(div_test['nodeId'], node_response['result']['nodeId'])
def test_dom_childlist_mutation(self):
doc_root = self.debugger.enable_dom()
# document: <html><head></head><body></body></html>
html_node = doc_root['children'][0]
head_node = html_node['children'][0]
body_node = html_node['children'][1]
self.assertEqual('BODY', body_node['nodeName'])
# getDocument should return HEAD and BODY, but none of their children.
self.assertEqual(3, head_node['childNodeCount']) # title, script, script
self.assertEqual(3, body_node['childNodeCount']) # h1, div, script
self.assertNotIn('children', head_node)
self.assertNotIn('children', body_node)
# Request 1 level of children in the BODY
self.debugger.run_command('DOM.requestChildNodes', {
'nodeId': body_node['nodeId'],
'depth': 1,
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
self.assertEqual(body_node['childNodeCount'],
len(child_nodes_event['params']['nodes']))
h1 = child_nodes_event['params']['nodes'][0]
div_test = child_nodes_event['params']['nodes'][1]
self.assertEqual(['id', 'test'], div_test['attributes'])
self.assertEqual(1, h1['childNodeCount']) # span
self.assertEqual(3, div_test['childNodeCount']) # div A, div B, span C
self.assertNotIn('children', h1)
self.assertNotIn('children', div_test)
# Insert a node as a child of a node whose children aren't yet reported.
self.debugger.evaluate_js(
'elem = document.createElement("span");'
'elem.id = "child";'
'elem.textContent = "foo";'
'document.getElementById("test").appendChild(elem);')
count_event = self.debugger.wait_event('DOM.childNodeCountUpdated')
self.assertEqual(div_test['nodeId'], count_event['params']['nodeId'])
self.assertEqual(div_test['childNodeCount'] + 1,
count_event['params']['childNodeCount'])
# Remove a child from a node whose children aren't yet reported.
self.debugger.evaluate_js('elem = document.getElementById("test");'
'elem.removeChild(elem.lastChild);')
count_event = self.debugger.wait_event('DOM.childNodeCountUpdated')
self.assertEqual(div_test['nodeId'], count_event['params']['nodeId'])
self.assertEqual(div_test['childNodeCount'],
count_event['params']['childNodeCount'])
# Request the children of the test div to repeat the insert/remove tests
# after its children have been reported.
self.debugger.run_command('DOM.requestChildNodes', {
'nodeId': div_test['nodeId'],
'depth': 1,
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
self.assertEqual(div_test['nodeId'],
child_nodes_event['params']['parentId'])
self.assertEqual(div_test['childNodeCount'],
len(child_nodes_event['params']['nodes']))
# Insert a node as a child of a node whose children have been reported.
self.debugger.evaluate_js(
'elem = document.createElement("span");'
'elem.id = "child";'
'elem.textContent = "foo";'
'document.getElementById("test").appendChild(elem);')
inserted_event = self.debugger.wait_event('DOM.childNodeInserted')
self.assertEqual(div_test['nodeId'],
inserted_event['params']['parentNodeId'])
self.assertEqual(child_nodes_event['params']['nodes'][-1]['nodeId'],
inserted_event['params']['previousNodeId'])
# Remove a child from a node whose children have been reported.
self.debugger.evaluate_js('elem = document.getElementById("test");'
'elem.removeChild(elem.lastChild);')
removed_event = self.debugger.wait_event('DOM.childNodeRemoved')
self.assertEqual(div_test['nodeId'],
removed_event['params']['parentNodeId'])
self.assertEqual(inserted_event['params']['node']['nodeId'],
removed_event['params']['nodeId'])
# Move a subtree to another part of the DOM that has not yet been reported.
# (Get the original children of <div#test> to depth 1)
self.debugger.run_command('DOM.requestChildNodes', {
'nodeId': div_test['nodeId'],
'depth': 1,
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
orig_num_children = len(child_nodes_event['params']['nodes'])
orig_div_a = child_nodes_event['params']['nodes'][0]
orig_span_c = child_nodes_event['params']['nodes'][2]
self.assertEqual(['id', 'A'], orig_div_a['attributes'])
self.assertEqual(['id', 'C'], orig_span_c['attributes'])
# (Move <span#C> into <div#A>)
self.debugger.evaluate_js('a = document.getElementById("A");'
'c = document.getElementById("C");'
'a.appendChild(c);')
removed_event = self.debugger.wait_event('DOM.childNodeRemoved')
self.assertEqual(orig_span_c['nodeId'], removed_event['params']['nodeId'])
count_event = self.debugger.wait_event('DOM.childNodeCountUpdated')
self.assertEqual(orig_div_a['nodeId'], count_event['params']['nodeId'])
self.assertEqual(orig_div_a['childNodeCount'] + 1,
count_event['params']['childNodeCount'])
# (Check the moved children of <div#test>)
self.debugger.run_command('DOM.requestChildNodes', {
'nodeId': div_test['nodeId'],
'depth': 2,
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
div_a = child_nodes_event['params']['nodes'][0]
moved_span_c = div_a['children'][2]
self.assertEqual(orig_num_children - 1,
len(child_nodes_event['params']['nodes']))
self.assertEqual(['id', 'C'], moved_span_c['attributes'])
self.assertNotEqual(orig_span_c['nodeId'], moved_span_c['nodeId'])
# Move a subtree to another part of the DOM that has already been reported.
# (Move <div#B> into <div#A>)
orig_div_b = child_nodes_event['params']['nodes'][1]
orig_span_b1 = orig_div_b['children'][0]
self.debugger.evaluate_js('a = document.getElementById("A");'
'b = document.getElementById("B");'
'a.appendChild(b);')
removed_event = self.debugger.wait_event('DOM.childNodeRemoved')
self.assertEqual(orig_div_b['nodeId'], removed_event['params']['nodeId'])
inserted_event = self.debugger.wait_event('DOM.childNodeInserted')
moved_div_b = inserted_event['params']['node']
self.assertEqual(['id', 'B'], moved_div_b['attributes'])
self.assertNotEqual(orig_div_b['nodeId'], moved_div_b['nodeId'])
# (Check the children of moved <div#B>)
self.debugger.run_command('DOM.requestChildNodes', {
'nodeId': moved_div_b['nodeId'],
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
moved_span_b1 = child_nodes_event['params']['nodes'][0]
self.assertEqual(['id', 'B1'], moved_span_b1['attributes'])
self.assertNotEqual(orig_span_b1['nodeId'], moved_span_b1['nodeId'])
# Replace a subtree with innerHTML
# (replace all children of <div#B>)
inner_html = "<div id='D'>\\n</div>"
self.debugger.evaluate_js('b = document.getElementById("B");'
f'b.innerHTML = "{inner_html}"')
removed_event = self.debugger.wait_event('DOM.childNodeRemoved')
self.assertEqual(moved_span_b1['nodeId'], removed_event['params']['nodeId'])
inserted_event = self.debugger.wait_event('DOM.childNodeInserted')
self.assertEqual(moved_div_b['nodeId'],
inserted_event['params']['parentNodeId'])
div_d = inserted_event['params']['node']
self.assertEqual(['id', 'D'], div_d['attributes'])
self.assertEqual(0, div_d['childNodeCount']) # Whitespace not reported.
def test_dom_text_mutation(self):
doc_root = self.debugger.enable_dom()
# document: <html><head></head><body></body></html>
html_node = doc_root['children'][0]
body_node = html_node['children'][1]
# Request 2 levels of children in the BODY
self.debugger.run_command('DOM.requestChildNodes', {
'nodeId': body_node['nodeId'],
'depth': 2,
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
div_test = child_nodes_event['params']['nodes'][1]
# Unrequested lone text node at depth+1 in <h1><span>.
h1 = child_nodes_event['params']['nodes'][0]
h1_span = h1['children'][0]
h1_text = h1_span['children'][0]
self.assertEqual(h1['childNodeCount'], len(h1['children']))
self.assertEqual(3, h1_text['nodeType']) # Text
self.assertEqual('Web debugger', h1_text['nodeValue'])
# Unrequested lone whitespace text node at depth+1 in <span#B1>
div_b = div_test['children'][1]
self.debugger.run_command('DOM.requestChildNodes', {
'nodeId': div_b['nodeId'],
'depth': 1,
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
span_b1 = child_nodes_event['params']['nodes'][0]
text_b1 = span_b1['children'][0]
self.assertEqual(3, text_b1['nodeType']) # Text
self.assertEqual('', text_b1['nodeValue'].strip())
# Changing a text node's value mutates character data.
self.debugger.evaluate_js('text = document.getElementById("B1").firstChild;'
'text.nodeValue = "Hello";')
data_event = self.debugger.wait_event('DOM.characterDataModified')
self.assertEqual(text_b1['nodeId'], data_event['params']['nodeId'])
self.assertEqual('Hello', data_event['params']['characterData'])
# Setting whitespace in a lone text node reports it removed.
self.debugger.evaluate_js('text = document.getElementById("B1").firstChild;'
'text.nodeValue = "";')
removed_event = self.debugger.wait_event('DOM.childNodeRemoved')
self.assertEqual(span_b1['nodeId'], removed_event['params']['parentNodeId'])
self.assertEqual(text_b1['nodeId'], removed_event['params']['nodeId'])
# Setting text in a lone whitespace text node reports it inserted.
self.debugger.evaluate_js('text = document.getElementById("B1").firstChild;'
'text.nodeValue = "Revived";')
inserted_event = self.debugger.wait_event('DOM.childNodeInserted')
self.assertEqual(span_b1['nodeId'],
inserted_event['params']['parentNodeId'])
self.assertEqual(0, inserted_event['params']['previousNodeId'])
self.assertEqual(3, inserted_event['params']['node']['nodeType']) # Text
self.assertEqual('Revived', inserted_event['params']['node']['nodeValue'])
self.assertNotEqual(text_b1['nodeId'],
inserted_event['params']['node']['nodeId'])
# Setting text in a whitespace sibling text node reports it inserted.
self.debugger.evaluate_js(
'text = document.getElementById("B1").nextSibling;'
'text.nodeValue = "Filled";')
inserted_event = self.debugger.wait_event('DOM.childNodeInserted')
self.assertEqual(div_b['nodeId'], inserted_event['params']['parentNodeId'])
self.assertEqual(span_b1['nodeId'],
inserted_event['params']['previousNodeId'])
self.assertEqual(3, inserted_event['params']['node']['nodeType']) # Text
self.assertEqual('Filled', inserted_event['params']['node']['nodeValue'])
self.assertNotEqual(text_b1['nodeId'],
inserted_event['params']['node']['nodeId'])
# Setting whitespace in a sibling text node reports it removed.
self.debugger.evaluate_js(
'text = document.getElementById("B1").nextSibling;'
'text.nodeValue = "";')
removed_event = self.debugger.wait_event('DOM.childNodeRemoved')
self.assertEqual(div_b['nodeId'], removed_event['params']['parentNodeId'])
self.assertEqual(inserted_event['params']['node']['nodeId'],
removed_event['params']['nodeId'])
# Setting textContent removes all children and inserts a new text node.
self.debugger.evaluate_js(
'document.getElementById("B").textContent = "One";')
removed_event = self.debugger.wait_event('DOM.childNodeRemoved')
self.assertEqual(div_b['nodeId'], removed_event['params']['parentNodeId'])
self.assertEqual(span_b1['nodeId'], removed_event['params']['nodeId'])
inserted_event = self.debugger.wait_event('DOM.childNodeInserted')
self.assertEqual(div_b['nodeId'], inserted_event['params']['parentNodeId'])
self.assertEqual(0, inserted_event['params']['previousNodeId'])
self.assertEqual(3, inserted_event['params']['node']['nodeType']) # Text
self.assertEqual('One', inserted_event['params']['node']['nodeValue'])
# Setting empty textContent removes all children without inserting anything.
self.debugger.evaluate_js('document.getElementById("B").textContent = "";')
removed_event = self.debugger.wait_event('DOM.childNodeRemoved')
self.assertEqual(div_b['nodeId'], removed_event['params']['parentNodeId'])
self.assertEqual(inserted_event['params']['node']['nodeId'],
removed_event['params']['nodeId'])
# Setting textContent over empty text only inserts a new text node.
self.debugger.evaluate_js(
'document.getElementById("B").textContent = "Two";')
inserted_event = self.debugger.wait_event('DOM.childNodeInserted')
self.assertEqual(div_b['nodeId'], inserted_event['params']['parentNodeId'])
self.assertEqual(0, inserted_event['params']['previousNodeId'])
self.assertEqual(3, inserted_event['params']['node']['nodeType']) # Text
self.assertEqual('Two', inserted_event['params']['node']['nodeValue'])
# Setting textContent over other text replaces the text nodes.
self.debugger.evaluate_js(
'document.getElementById("B").textContent = "Three";')
removed_event = self.debugger.wait_event('DOM.childNodeRemoved')
self.assertEqual(div_b['nodeId'], removed_event['params']['parentNodeId'])
self.assertEqual(inserted_event['params']['node']['nodeId'],
removed_event['params']['nodeId'])
inserted_event = self.debugger.wait_event('DOM.childNodeInserted')
self.assertEqual(div_b['nodeId'], inserted_event['params']['parentNodeId'])
self.assertEqual(0, inserted_event['params']['previousNodeId'])
self.assertEqual(3, inserted_event['params']['node']['nodeType']) # Text
self.assertEqual('Three', inserted_event['params']['node']['nodeValue'])
# Setting whitespace textContent removes all children without any insertion.
self.debugger.evaluate_js(
'document.getElementById("B").textContent = "\\n";')
removed_event = self.debugger.wait_event('DOM.childNodeRemoved')
self.assertEqual(div_b['nodeId'], removed_event['params']['parentNodeId'])
self.assertEqual(inserted_event['params']['node']['nodeId'],
removed_event['params']['nodeId'])
# Setting textContent over whitespace text only inserts a new text node.
self.debugger.evaluate_js(
'document.getElementById("B").textContent = "Four";')
inserted_event = self.debugger.wait_event('DOM.childNodeInserted')
self.assertEqual(div_b['nodeId'], inserted_event['params']['parentNodeId'])
self.assertEqual(0, inserted_event['params']['previousNodeId'])
self.assertEqual(3, inserted_event['params']['node']['nodeType']) # Text
self.assertEqual('Four', inserted_event['params']['node']['nodeValue'])
def test_dom_attribute_mutation(self):
doc_root = self.debugger.enable_dom()
# document: <html><head></head><body></body></html>
html_node = doc_root['children'][0]
body_node = html_node['children'][1]
# Request 1 level of children in the BODY
self.debugger.run_command('DOM.requestChildNodes', {
'nodeId': body_node['nodeId'],
'depth': 1,
})
child_nodes_event = self.debugger.wait_event('DOM.setChildNodes')
h1 = child_nodes_event['params']['nodes'][0]
def assert_attr_modified(statement, name, value):
self.debugger.evaluate_js(
'elem = document.getElementsByTagName("H1")[0];' + statement)
attr_event = self.debugger.wait_event('DOM.attributeModified')
self.assertEqual(attr_event['params']['nodeId'], h1['nodeId'])
self.assertEqual(attr_event['params']['name'], name)
self.assertEqual(attr_event['params']['value'], value)
def assert_attr_removed(statement, name):
self.debugger.evaluate_js(
'elem = document.getElementsByTagName("H1")[0];' + statement)
attr_event = self.debugger.wait_event('DOM.attributeRemoved')
self.assertEqual(attr_event['params']['nodeId'], h1['nodeId'])
self.assertEqual(attr_event['params']['name'], name)
# Add a normal attribute.
assert_attr_modified('elem.id = "foo"', 'id', 'foo')
# Change a normal attribute.
assert_attr_modified('elem.id = "bar"', 'id', 'bar')
# Change a normal attribute with setAttribute.
assert_attr_modified('elem.setAttribute("id", "baz")', 'id', 'baz')
# Remove a normal attribute.
assert_attr_removed('elem.removeAttribute("id")', 'id')
# Change the className (from 'name', as set in web_debugger.html).
assert_attr_modified('elem.className = "zzyzx"', 'class', 'zzyzx')
# Change a dataset value (from 'robot', as set in web_debugger.html).
assert_attr_modified('elem.dataset.who = "person"', 'data-who', 'person')
# Add a dataset value.
assert_attr_modified('elem.dataset.what = "thing"', 'data-what', 'thing')
# Remove a data attribute with delete operator.
# TODO: Fix this - MutationObserver is not reporting this mutation,
# assert_attr_removed('delete elem.dataset.who', 'data-who')
# Change a data attribute with setAttribute.
assert_attr_modified('elem.setAttribute("data-what", "object")',
'data-what', 'object')
# Add a data attribute with setAttribute.
assert_attr_modified('elem.setAttribute("data-where", "place")',
'data-where', 'place')
# Remove a data attribute with removeAttribute.
assert_attr_removed('elem.removeAttribute("data-what")', 'data-what')
def assert_paused(self, expected_stacks):
"""Checks that the debugger is paused at a breakpoint.
Waits for the expected |Debugger.paused| event from hitting the breakpoint
and then asserts that the expected_stacks match the call stacks in that
event. Execution is always resumed before returning so that more JS can be
evaluated, as needed to continue or end the test.
Args:
expected_stacks: A list of lists of strings with the expected function
names in the call stacks in a series of asynchronous executions.
"""
# Expect an anonymous frame from the backend eval that's running the code
# we sent in the "Runtime.evaluate" command.
expected_stacks[-1] += ['']
paused_event = self.debugger.wait_event('Debugger.paused')
try:
call_stacks = []
# First the main stack where the breakpoint was hit.
call_frames = paused_event['params']['callFrames']
call_stack = [frame['functionName'] for frame in call_frames]
call_stacks.append(call_stack)
# Then asynchronous stacks that preceded the main stack.
async_trace = paused_event['params'].get('asyncStackTrace')
while async_trace:
call_frames = async_trace['callFrames']
call_stack = [frame['functionName'] for frame in call_frames]
call_stacks.append(call_stack)
async_trace = async_trace.get('parent')
self.assertEqual(expected_stacks, call_stacks)
finally:
# We must resume in order to avoid hanging if something goes wrong.
self.debugger.run_command('Debugger.resume')
self.debugger.wait_event('Debugger.resumed')
def test_debugger_breakpoint(self):
self.debugger.run_command('Debugger.enable')
# Get the ID and source of our JavaScript test utils.
script_id = ''
while not script_id:
script_event = self.debugger.wait_event('Debugger.scriptParsed')
script_url = script_event['params']['url']
if script_url.endswith('web_debugger_test_utils.js'):
script_id = script_event['params']['scriptId']
source_response = self.debugger.run_command('Debugger.getScriptSource', {
'scriptId': script_id,
})
script_source = source_response['result']['scriptSource'].splitlines()
# Set a breakpoint on the asyncBreak() function.
line_number = next(n for n, l in enumerate(script_source)
if l.startswith('function asyncBreak'))
self.debugger.run_command('Debugger.setBreakpoint', {
'location': {
'scriptId': script_id,
'lineNumber': line_number,
},
})
self.debugger.run_command('Debugger.setAsyncCallStackDepth', {
'maxDepth': 99,
})
# Check the breakpoint within a SetTimeout() callback.
self.debugger.evaluate_js('testSetTimeout()')
self.assert_paused([
[
'asyncBreak',
'asyncC',
'timeoutB',
],
[
'asyncB',
'timeoutA',
],
[
'asyncA',
'testSetTimeout',
],
])
# Check the breakpoint within a promise "then" after being resolved.
self.debugger.evaluate_js('testPromise()')
self.assert_paused([
[
'asyncBreak',
'promiseThen',
'promiseTimeout',
],
[
'waitPromise',
'testPromise',
],
])
# Check the breakpoint after async await for a promise to resolve.
self.debugger.evaluate_js('testAsyncFunction()')
self.assert_paused([
[
'asyncBreak',
'asyncAwait',
'promiseTimeout',
],
[
'asyncAwait',
'testAsyncFunction',
],
])
# Check the breakpoint within an XHR event handler.
self.debugger.evaluate_js('testXHR(window.location.href)')
self.assert_paused([
[
'asyncBreak',
'fileLoaded',
],
[
'doXHR',
'testXHR',
],
])
# Check the breakpoint within a MutationObserver.
self.debugger.evaluate_js('testMutate()')
self.assert_paused([
[
'asyncBreak',
'mutationCallback',
],
[
'doSetAttribute',
'testMutate',
],
])
# Check the breakpoint within an animation callback.
self.debugger.evaluate_js('testAnimationFrame()')
self.assert_paused([
[
'asyncBreak',
'animationFrameCallback',
],
[
'doRequestAnimationFrame',
'testAnimationFrame',
],
])
# Check the breakpoint on a media callback going through EventQueue.
self.debugger.evaluate_js('testMediaSource()')
self.assert_paused([
[
'asyncBreak',
'sourceOpenCallback',
],
[
'setSourceListener',
'testMediaSource',
],
])