Unverified Commit b8a89e6d authored by Ian Briggs's avatar Ian Briggs Committed by GitHub
Browse files

Merge pull request #9 from utah-cs4962-fa21/chapter9

Chapter9
parents 7d6b565a 7979f575
Tests for WBE Chapter 9
=======================
Chapter 9 (Running Interactive Scripts) introduces JavaScript and the DOM API,
The focus of the chapter is browser-JS
interaction.
>>> import test
>>> _ = test.socket.patch().start()
>>> _ = test.ssl.patch().start()
>>> test.NORMALIZE_FONT = True
>>> import browser
Note that we aren't mocking `dukpy`. It should just run JavaScript normally!
Testing basic <script> support
==============================
The browser should download JavaScript code mentioned in a `<script>` tag:
>>> url = 'http://test.test/chapter9-base/html'
>>> url2 = 'http://test.test/chapter9-base/js'
>>> html_page = "<script src=" + url2 + "></script>"
>>> test.socket.respond_200(url, body=html_page)
>>> test.socket.respond_200(url2, body="")
>>> browser.Browser().load(url)
>>> req = test.socket.last_request(url2).decode("utf-8").lower()
>>> req.startswith("get")
True
>>> req.split()[1]
'/chapter9-base/js'
If the script succeeds, the browser prints nothing:
>>> test.socket.respond_200(url2, body="var x = 2; x + x")
>>> browser.Browser().load(url)
If instead the script crashes, the browser prints an error message:
>>> test.socket.respond_200(url2, body="throw Error('Oops');")
>>> browser.Browser().load(url) #doctest: +ELLIPSIS
Script http://test.test/chapter9-base/js crashed Error: Oops
...
Note that in the last test I set the `ELLIPSIS` flag to elide the duktape stack
trace.
Testing JSContext
=================
For the rest of these tests we're going to use `console.log` for most testing:
>>> test.socket.respond_200(url2, body="console.log('Hello, world!')")
>>> browser.Browser().load(url)
Hello, world!
Note that you can print other data structures as well:
>>> test.socket.respond_200(url2, body="console.log([2, 3, 4])")
>>> browser.Browser().load(url)
[2, 3, 4]
Let's test that variables work:
>>> test.socket.respond_200(url2, body="var x = 'Hello!'; console.log(x)")
>>> browser.Browser().load(url)
Hello!
Next let's try to do two scripts:
>>> url2 = 'http://test.test/chapter9-base/js1'
>>> url3 = 'http://test.test/chapter9-base/js2'
>>> html_page = "<script src=" + url2 + "></script>" + "<script src=" + url3 + "></script>"
>>> test.socket.respond_200(url, body=html_page)
>>> test.socket.respond_200(url2, body="var x = 'Testing, testing';")
>>> test.socket.respond_200(url3, body="console.log(x);")
>>> browser.Browser().load(url)
Testing, testing
Testing querySelectorAll
========================
The `querySelectorAll` method is easiest to test by looking at the number of
matching nodes:
>>> page = """<!doctype html>
... <div>
... <p id=lorem>Lorem</p>
... <p class=ipsum>Ipsum</p>
... </div>"""
>>> test.socket.respond_200(url, body=page)
>>> b = browser.Browser()
>>> b.load(url)
>>> js = b.tabs[0].js
>>> js.run("document.querySelectorAll('div').length")
1
>>> js.run("document.querySelectorAll('p').length")
2
>>> js.run("document.querySelectorAll('html').length")
1
That last query is finding an implicit tag. Complex queries are also supported
>>> js.run("document.querySelectorAll('html p').length")
2
>>> js.run("document.querySelectorAll('html body div p').length")
2
>>> js.run("document.querySelectorAll('body html div p').length")
0
Testing getAttribute
====================
`querySelectorAll` should return `Node` objects:
>>> js.run("document.querySelectorAll('html')[0] instanceof Node")
True
Once we have a `Node` object we can call `getAttribute`:
>>> js.run("document.querySelectorAll('p')[0].getAttribute('id')")
'lorem'
Note that this is "live": as the page changes `querySelectorAll` gives new results:
>>> b.tabs[0].nodes.children[0].children[0].children[0].attributes['id'] = 'blah'
>>> js.run("document.querySelectorAll('p')[0].getAttribute('id')")
'blah'
Testing innerHTML
=================
Testing `innerHTML` is tricky because it knowingly misbehaves on hard-to-parse
HTML fragments. So we must purposely avoid testing those.
One annoying thing about `innerHTML` is that, since it is an assignment, it
returns its right hand side. I use `void()` to avoid testing that.
>>> js.run("void(document.querySelectorAll('p')[0].innerHTML" +
... " = 'This is a <b id=wen>new</b> element!')")
Once we've changed the page, the browser should rerender:
>>> browser.print_tree(b.tabs[0].document)
DocumentLayout()
BlockLayout(x=13, y=18, width=774, height=30.0)
BlockLayout(x=13, y=18, width=774, height=30.0)
BlockLayout(x=13, y=18, width=774, height=30.0)
InlineLayout(x=13, y=18, width=774, height=15.0)
LineLayout(x=13, y=18, width=774, height=15.0)
TextLayout(x=13, y=20.25, width=48, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=73, y=20.25, width=24, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=109, y=20.25, width=12, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=133, y=20.25, width=36, height=12, font=Font size=12 weight=bold slant=roman style=None)
TextLayout(x=181, y=20.25, width=96, height=12, font=Font size=12 weight=normal slant=roman style=None)
InlineLayout(x=13, y=33.0, width=774, height=15.0)
LineLayout(x=13, y=33.0, width=774, height=15.0)
TextLayout(x=13, y=35.25, width=60, height=12, font=Font size=12 weight=normal slant=roman style=None)
Note that there's now many `TextLayout`s inside the first `LineLayout`, one per
new word.
Now that we've modified the page we should be able to find the new elements:
>>> js.run("document.querySelectorAll('b').length")
1
We should also be able to delete nodes this way:
>>> js.run("var old_b = document.querySelectorAll('b')[0]")
>>> js.run("void(document.querySelectorAll('p')[0].innerHTML = 'Lorem')")
>>> js.run("document.querySelectorAll('b').length")
0
The page is rerendered again:
>>> browser.print_tree(b.tabs[0].document)
DocumentLayout()
BlockLayout(x=13, y=18, width=774, height=30.0)
BlockLayout(x=13, y=18, width=774, height=30.0)
BlockLayout(x=13, y=18, width=774, height=30.0)
InlineLayout(x=13, y=18, width=774, height=15.0)
LineLayout(x=13, y=18, width=774, height=15.0)
TextLayout(x=13, y=20.25, width=60, height=12, font=Font size=12 weight=normal slant=roman style=None)
InlineLayout(x=13, y=33.0, width=774, height=15.0)
LineLayout(x=13, y=33.0, width=774, height=15.0)
TextLayout(x=13, y=35.25, width=60, height=12, font=Font size=12 weight=normal slant=roman style=None)
Despite this, the old nodes should stick around:
>>> js.run("old_b.getAttribute('id')")
'wen'
Testing events
==============
Events are the trickiest thing to test here. First, let's do a basic test of
adding an event listener and then triggering it. I'll use the `div` element to
test things:
>>> div = b.tabs[0].nodes.children[0].children[0]
>>> js.run("var div = document.querySelectorAll('div')[0]")
>>> js.run("div.addEventListener('test', function(e) { console.log('Listener ran!')})")
>>> js.dispatch_event("test", div)
Listener ran!
False
The `False` is from our `preventDefault` handling (we didn't call it).
Let's test each of our automatic event types. We'll need a new web page with a
link, a button, and an input area:
>>> page = """<!doctype html>
... <a href=page2>Click me!</a>
... <form action=/post>
... <input name=input value=hi>
... <button>Submit</button>
... </form>"""
>>> test.socket.respond_200(url, body=page)
>>> b.load(url)
>>> js = b.tabs[1].js
Now we're going test five event handlers: clicking on the link, clicking on the
input, typing into the input, clicking on the button, and submitting the form.
We'll have a mix of `preventDefault` and non-`preventDefault` handlers to test
that feature as well.
>>> js.run("var a = document.querySelectorAll('a')[0]")
>>> js.run("var form = document.querySelectorAll('form')[0]")
>>> js.run("var input = document.querySelectorAll('input')[0]")
>>> js.run("var button = document.querySelectorAll('button')[0]")
Note that the `input` element has a value of `hi`:
>>> js.run("input.getAttribute('value')")
'hi'
Clicking on the link should be cancelled because we don't actually want to
navigate to a new page.
>>> js.run("a.addEventListener('click', " +
... "function(e) { console.log('a clicked'); e.preventDefault()})")
For the `input` element, clicking should work, because we need to focus it to
type into it. But let's cancel the `keydown` event just to test that that works.
>>> js.run("input.addEventListener('click', " +
... "function(e) { console.log('input clicked')})")
>>> js.run("input.addEventListener('keydown', " +
... "function(e) { console.log('input typed'); e.preventDefault()})")
Finally, let's allow clicking on the button but then cancel the form submission:
>>> js.run("button.addEventListener('click', " +
... "function(e) { console.log('button clicked')})")
>>> js.run("form.addEventListener('submit', " +
... "function(e) { console.log('form submitted'); e.preventDefault()})")
With these all set up, we need to do some clicking and typing to trigger these
events. The display list gives us coordinates for clicking.
>>> browser.print_tree(b.tabs[1].document)
DocumentLayout()
BlockLayout(x=13, y=18, width=774, height=30.0)
BlockLayout(x=13, y=18, width=774, height=30.0)
InlineLayout(x=13, y=18, width=774, height=15.0)
LineLayout(x=13, y=18, width=774, height=15.0)
TextLayout(x=13, y=20.25, width=60, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=85, y=20.25, width=36, height=12, font=Font size=12 weight=normal slant=roman style=None)
InlineLayout(x=13, y=33.0, width=774, height=15.0)
LineLayout(x=13, y=33.0, width=774, height=15.0)
InputLayout(x=13, y=35.25, width=200, height=12)
InputLayout(x=225, y=35.25, width=200, height=12)
>>> b.tabs[1].click(14, 20)
a clicked
>>> b.tabs[1].click(14, 40)
input clicked
>>> b.tabs[1].keypress('t')
input typed
>>> b.tabs[1].click(230, 40)
button clicked
form submitted
However, we should not have navigated away from the original URL, because we
prevented submission:
>>> b.tabs[1].history[-1]
'http://test.test/chapter9-base/html'
Similarly, when we clicked on the `input` element its `value` should be cleared,
but when we then typed `t` into it that was cancelled so the value should still
be empty at the end:
>>> js.run("input.getAttribute('value')")
''
Tests for WBE Chapter 9 Exercise `createElement`
============================================
Description
-----------
The `document.createElement` method creates a new element, which can be attached
to the document with the `appendChild` and `insertBefore` methods on Nodes;
unlike innerHTML, there’s no parsing involved.
Implement all three methods.
Extra Requirements
------------------
* We will only call `appendChild` and `insertBefore` to add newly created
elements using valid arguments.
Test code
---------
Boilerplate.
>>> import test
>>> _ = test.socket.patch().start()
>>> _ = test.ssl.patch().start()
>>> test.NORMALIZE_FONT = True
>>> import browser
Show the page with no content changes by scripts.
>>> web_url = 'http://test.test/chapter9-create-1/html'
>>> html = "<div>Some content</div> <p>More content</p>"
>>> test.socket.respond_200(web_url, body=html)
>>> this_browser = browser.Browser()
>>> this_browser.load(web_url)
>>> browser.print_tree(this_browser.tabs[0].document)
DocumentLayout()
BlockLayout(x=13, y=18, width=774, height=30.0)
BlockLayout(x=13, y=18, width=774, height=30.0)
InlineLayout(x=13, y=18, width=774, height=15.0)
LineLayout(x=13, y=18, width=774, height=15.0)
TextLayout(x=13, y=20.25, width=48, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=73, y=20.25, width=84, height=12, font=Font size=12 weight=normal slant=roman style=None)
InlineLayout(x=13, y=33.0, width=774, height=15.0)
LineLayout(x=13, y=33.0, width=774, height=15.0)
TextLayout(x=13, y=35.25, width=48, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=73, y=35.25, width=84, height=12, font=Font size=12 weight=normal slant=roman style=None)
Set up the webpage and script links.
Create a button and add it as a child to the `<body>` at the end.
>>> web_url = 'http://test.test/chapter9-create-2/html'
>>> script_url = 'http://test.test/chapter9-create-2/js'
>>> html = ("<script src=" + script_url + "></script>"
... + "<div>Some content</div> <p>More content</p>")
>>> test.socket.respond_200(web_url, body=html)
>>> script = """
... new_elt = document.createElement("button");
... my_body = document.querySelectorAll('body')[0];
... my_body.appendChild(new_elt);
... """
>>> test.socket.respond_200(script_url, body=script)
>>> this_browser = browser.Browser()
>>> this_browser.load(web_url)
>>> browser.print_tree(this_browser.tabs[0].document)
DocumentLayout()
BlockLayout(x=13, y=18, width=774, height=30.0)
InlineLayout(x=13, y=18, width=774, height=0)
LineLayout(x=13, y=18, width=774, height=0)
BlockLayout(x=13, y=18, width=774, height=30.0)
InlineLayout(x=13, y=18, width=774, height=15.0)
LineLayout(x=13, y=18, width=774, height=15.0)
TextLayout(x=13, y=20.25, width=48, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=73, y=20.25, width=84, height=12, font=Font size=12 weight=normal slant=roman style=None)
InlineLayout(x=13, y=33.0, width=774, height=15.0)
LineLayout(x=13, y=33.0, width=774, height=15.0)
TextLayout(x=13, y=35.25, width=48, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=73, y=35.25, width=84, height=12, font=Font size=12 weight=normal slant=roman style=None)
BlockLayout(x=13, y=48.0, width=774, height=0)
Create a button and add it inside the `<body>` before the `<div>`
>>> web_url = 'http://test.test/chapter9-create-3/html'
>>> script_url = 'http://test.test/chapter9-create-3/js'
>>> html = ("<script src=" + script_url + "></script>"
... + "<div>Some content</div> <p>More content</p>")
>>> test.socket.respond_200(web_url, body=html)
>>> script = """
... new_elt = document.createElement("button");
... my_body = document.querySelectorAll('body')[0];
... my_div = document.querySelectorAll('div')[0];
... my_body.insertBefore(new_elt, my_div);
... """
>>> test.socket.respond_200(script_url, body=script)
>>> this_browser = browser.Browser()
>>> this_browser.load(web_url)
>>> browser.print_tree(this_browser.tabs[0].document)
DocumentLayout()
BlockLayout(x=13, y=18, width=774, height=30.0)
InlineLayout(x=13, y=18, width=774, height=0)
LineLayout(x=13, y=18, width=774, height=0)
BlockLayout(x=13, y=18, width=774, height=30.0)
BlockLayout(x=13, y=18, width=774, height=0)
InlineLayout(x=13, y=18, width=774, height=15.0)
LineLayout(x=13, y=18, width=774, height=15.0)
TextLayout(x=13, y=20.25, width=48, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=73, y=20.25, width=84, height=12, font=Font size=12 weight=normal slant=roman style=None)
InlineLayout(x=13, y=33.0, width=774, height=15.0)
LineLayout(x=13, y=33.0, width=774, height=15.0)
TextLayout(x=13, y=35.25, width=48, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=73, y=35.25, width=84, height=12, font=Font size=12 weight=normal slant=roman style=None)
Create a button and add it inside the `<body>` before the `<p>`
>>> web_url = 'http://test.test/chapter9-create-4/html'
>>> script_url = 'http://test.test/chapter9-create-4/js'
>>> html = ("<script src=" + script_url + "></script>"
... + "<div>Some content</div> <p>More content</p>")
>>> test.socket.respond_200(web_url, body=html)
>>> script = """
... new_elt = document.createElement("button");
... my_body = document.querySelectorAll('body')[0];
... my_p = document.querySelectorAll('p')[0];
... my_body.insertBefore(new_elt, my_p);
... """
>>> test.socket.respond_200(script_url, body=script)
>>> this_browser = browser.Browser()
>>> this_browser.load(web_url)
>>> browser.print_tree(this_browser.tabs[0].document)
DocumentLayout()
BlockLayout(x=13, y=18, width=774, height=30.0)
InlineLayout(x=13, y=18, width=774, height=0)
LineLayout(x=13, y=18, width=774, height=0)
BlockLayout(x=13, y=18, width=774, height=30.0)
InlineLayout(x=13, y=18, width=774, height=15.0)
LineLayout(x=13, y=18, width=774, height=15.0)
TextLayout(x=13, y=20.25, width=48, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=73, y=20.25, width=84, height=12, font=Font size=12 weight=normal slant=roman style=None)
BlockLayout(x=13, y=33.0, width=774, height=0)
InlineLayout(x=13, y=33.0, width=774, height=15.0)
LineLayout(x=13, y=33.0, width=774, height=15.0)
TextLayout(x=13, y=35.25, width=48, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=73, y=35.25, width=84, height=12, font=Font size=12 weight=normal slant=roman style=None)
Create a button and add it inside the `<body>` at then end of its children
by using `insertBefore` with a reference node of null.
>>> web_url = 'http://test.test/chapter9-create-5/html'
>>> script_url = 'http://test.test/chapter9-create-5/js'
>>> html = ("<script src=" + script_url + "></script>"
... + "<div>Some content</div> <p>More content</p>")
>>> test.socket.respond_200(web_url, body=html)
>>> script = """
... new_elt = document.createElement("button");
... my_body = document.querySelectorAll('body')[0];
... my_body.insertBefore(new_elt, null);
... """
>>> test.socket.respond_200(script_url, body=script)
>>> this_browser = browser.Browser()
>>> this_browser.load(web_url)
>>> browser.print_tree(this_browser.tabs[0].document)
DocumentLayout()
BlockLayout(x=13, y=18, width=774, height=30.0)
InlineLayout(x=13, y=18, width=774, height=0)
LineLayout(x=13, y=18, width=774, height=0)
BlockLayout(x=13, y=18, width=774, height=30.0)
InlineLayout(x=13, y=18, width=774, height=15.0)
LineLayout(x=13, y=18, width=774, height=15.0)
TextLayout(x=13, y=20.25, width=48, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=73, y=20.25, width=84, height=12, font=Font size=12 weight=normal slant=roman style=None)
InlineLayout(x=13, y=33.0, width=774, height=15.0)
LineLayout(x=13, y=33.0, width=774, height=15.0)
TextLayout(x=13, y=35.25, width=48, height=12, font=Font size=12 weight=normal slant=roman style=None)
TextLayout(x=73, y=35.25, width=84, height=12, font=Font size=12 weight=normal slant=roman style=None)
BlockLayout(x=13, y=48.0, width=774, height=0)
Tests for WBE Chapter 9 Exercise `Event Bubbling`
============================================
Description
-----------
Right now, you can attach a click handler to a elements, but not to anything
else.
Fix this.
One challenge you’ll face is that when you click on an element, you also click
on all its ancestors.
On the web, this sort of quirk is handled by event bubbling: when an event is
generated on an element, listeners are run not just on that element but
also on its ancestors.
Implement event bubbling, and make sure listeners can call stopPropagation on
the event object to stop bubbling the event up the tree.
Double-check that clicking on links still works, and make sure preventDefault
still successfully prevents clicks on a link from actually following the
link.
Test code
---------
Boilerplate.
>>> import test
>>> _ = test.socket.patch().start()
>>> _ = test.ssl.patch().start()
>>> test.NORMALIZE_FONT = True
>>> import browser
Set up the webpage and script links.
>>> web_url = 'http://test.test/chapter9-create-1/html'
>>> script_url = 'http://test.test/chapter9-create-1/js'
>>> html = ("<script src=" + script_url + "></script>"
... + "<div><form><input name=bubbles value=sugar></form></div>")
>>> test.socket.respond_200(web_url, body=html)
Attach an event listener to each nested element.
Click an show that all event listeners are called in the correct order.
>>> script = """
... document.querySelectorAll('div')[0].addEventListener('click',
... function(e) {
... console.log('div saw a click');
... });
... document.querySelectorAll('form')[0].addEventListener('click',
... function(e) {
... console.log('form saw a click');
... });
... document.querySelectorAll('input')[0].addEventListener('click',
... function(e) {
... console.log('input saw a click');
... });
... """
>>> test.socket.respond_200(script_url, body=script)
>>> this_browser = browser.Browser()
>>> this_browser.load(web_url)
>>> this_browser.handle_click(test.Event(20, 100 + 24))
input saw a click
form saw a click
div saw a click
>>> this_browser.tabs[0].js.run("document.querySelectorAll('input')[0].getAttribute('value')")
''
Setup a new webpage with the same content but a different script.
This time prevent the default in the input.
>>> web_url = 'http://test.test/chapter9-create-2/html'
>>> script_url = 'http://test.test/chapter9-create-2/js'
>>> html = ("<script src=" + script_url + "></script>"
... + "<div><form><input name=bubbles value=sugar></form></div>")
>>> test.socket.respond_200(web_url, body=html)
>>> script = """
... document.querySelectorAll('div')[0].addEventListener('click',
... function(e) {
... console.log('div saw a click');
... });
... document.querySelectorAll('form')[0].addEventListener('click',
... function(e) {
... console.log('form saw a click');
... });
... document.querySelectorAll('input')[0].addEventListener('click',
... function(e) {
... console.log('input saw a click');
... e.preventDefault();
... });
... """
>>> test.socket.respond_200(script_url, body=script)
>>> this_browser = browser.Browser()
>>> this_browser.load(web_url)
>>> this_browser.handle_click(test.Event(20, 100 + 24))
input saw a click
form saw a click
div saw a click
>>> this_browser.tabs[0].js.run("document.querySelectorAll('input')[0].getAttribute('value')")
'sugar'
Stopping propagation should also work.
>>> web_url = 'http://test.test/chapter9-create-3/html'
>>> script_url = 'http://test.test/chapter9-create-3/js'
>>> html = ("<script src=" + script_url + "></script>"
... + "<div><form><input name=bubbles value=sugar></form></div>")
>>> test.socket.respond_200(web_url, body=html)
>>> script = """
... document.querySelectorAll('div')[0].addEventListener('click',
... function(e) {
... console.log('div saw a click');
... });
... document.querySelectorAll('form')[0].addEventListener('click',
... function(e) {
... console.log('form saw a click');
... e.stopPropagation();
... });
... document.querySelectorAll('input')[0].addEventListener('click',
... function(e) {
... console.log('input saw a click');
... });
... """
>>> test.socket.respond_200(script_url, body=script)
>>> this_browser = browser.Browser()
>>> this_browser.load(web_url)
>>> this_browser.handle_click(test.Event(20, 100 + 24))
input saw a click
form saw a click
>>> this_browser.tabs[0].js.run("document.querySelectorAll('input')[0].getAttribute('value')")
''
Both should be able to work on the same click.
>>> web_url = 'http://test.test/chapter9-create-3/html'
>>> script_url = 'http://test.test/chapter9-create-3/js'
>>> html = ("<script src=" + script_url + "></script>"
... + "<div><form><input name=bubbles value=sugar></form></div>")
>>> test.socket.respond_200(web_url, body=html)
>>> script = """
... document.querySelectorAll('div')[0].addEventListener('click',
... function(e) {
... console.log('div saw a click');
... });
... document.querySelectorAll('form')[0].addEventListener('click',
... function(e) {
... console.log('form saw a click');
... });
... document.querySelectorAll('input')[0].addEventListener('click',
... function(e) {
... console.log('input saw a click');
... e.preventDefault();
... e.stopPropagation();
... });
... """
>>> test.socket.respond_200(script_url, body=script)
>>> this_browser = browser.Browser()
>>> this_browser.load(web_url)