# 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',
        ],
    ])
