Commit Graph

222 Commits

Author SHA1 Message Date
JoelEinbinder
5f0afd5547 Move uploadFiles onto ElementHandle (#348)
This patch:
- removes the `page.uploadFile` method
- adds `elementHandle.uploadFile` method.

Motivation: `elementHandle.uploadFile` is rarely used, so it doesn't worth it
to keep it on page.
2017-08-17 14:53:37 -07:00
Andrey Lushnikov
0a190ebec9 Rename 'remoteDebuggingURL' into 'browserWSEndpoint' (#272) 2017-08-16 01:10:55 -07:00
Andrey Lushnikov
44c15220f9 Rename ElementHandle.release into ElementHandle.dispose (#284)
The ElementHandle.release feels related to ElementHandle.click.
The dispose is a more clear name.
2017-08-16 00:49:20 -07:00
JoelEinbinder
3c2aaaaeb0 Readme fixup part 1 (#278) 2017-08-15 23:04:16 -07:00
Aleksey
56eab00fa7 Roll chromium to r494365 (#271)
This patch:
- rolls chromium to r494365
- starts using Runtime.evaluate(awaitPromise: true), with new semantic
  we can avoid additional Runtime.awaitPromise call
- stops resolving promises for Console event
2017-08-15 17:35:59 -07:00
Andrey Lushnikov
af89e893e7 Implement basic element handles (#248)
This patch implements basic element handles which a backed with remote objects.

Fixes #111
2017-08-15 14:54:02 -07:00
Andrey Lushnikov
a424f5613a Introduce Puppeteer.connect method (#264)
This patch:
- refactors Connection to use a single remote debugging URL instead of a
  pair of port and browserTargetId
- introduces Puppeteer.connect() method to attach to already running
  browser instance.

Fixes #238.
2017-08-15 14:29:42 -07:00
Andrey Lushnikov
96309a207c Fix request interception corner cases (#261)
This patch:
- teaches request interception to ignore data URLs. Currently protocol
  doesn't send interceptions for data URLs.
- teaches request interception to properly process URLs with hashes.
  Currently `Network.requestIntercepted` sends url with a hash, whereas
  `Network.requestWillBeSent` doesn't report hashes in its urls. @see
  crbug.com/755456
- skips one more header that I spotted during debugging interception on
  the realworld websites.

Fixes #258, #259.
2017-08-15 13:55:48 -07:00
Andrey Lushnikov
24764df4c4 Emit 'error' event when page crashes (#266)
This patch starts emitting 'error' event when page crashes.
'error' events have special treatment in node, so page crashes
become observable for users.

Fixes #262.
2017-08-15 11:13:05 -07:00
Andrey Lushnikov
13e8580a34 Use puppeteer.launch instead of browser constructor (#255)
This patch:
- split browser launching logic from Browser into `lib/Launcher.js`
- introduce `puppeteer` namespace which currently has a single `launch`
  method to start a browser

With this patch, the browser is no longer created with the `new
Browser(..)` command. Instead, it should be "launched" via the
`puppeteer.launch` method:

```js
const puppeteer = require('puppeteer');
puppeteer.launch().then(async browser => {
  ...
});
```

With this approach browser instance lifetime matches the lifetime of
actual browser process. This helps us:
- remove proxy streams, e.g. browser.stderr and browser.stdout
- cleanup browser class and make it possible to connect to remote
  browser
- introduce events on the browser instance, e.g. 'page' event. In case
  of lazy-launching browser, we should've launch browser when an event
  listener is added, which is unneded comlpexity.
2017-08-14 18:08:06 -07:00
Andrey Lushnikov
4551afc6dc Introduce new interception API (#242)
This patch introduces new interception API, via killing InterceptedRequest and giving the `abort` and `continue` methods to the Request object.
2017-08-11 17:24:31 -07:00
JoelEinbinder
d793e51a9a Add a test that uses Modernizr to detect touch (#245)
Add a test that uses Modernizr to detect touch
2017-08-11 13:59:23 -07:00
Andrey Lushnikov
dbac6788ae Plumb default prompt value to the dialog (#243)
This patch:
- plumbs default prompt value to the dialog
- enables one more phantom test
2017-08-11 12:17:43 -07:00
JoelEinbinder
497baf0198 Roll chromium to r493673 (#240)
Mouse events are no longer racy. Enabling touch no longer converts all mouse events into touches. Promises in destroyed execution contexts are rejected immediately.
2017-08-11 01:21:02 -07:00
Andrey Lushnikov
b8dbd28b8c Refer to the upstream bug (#239)
The issue #168 is a protocol inconsistency which happens only
in case of HTTPS error. This patch starts refering to the
upstream bug instead of puppeteer issue.

Closes #168.
2017-08-11 01:07:33 -07:00
Andrey Lushnikov
6347a049ba Rename Page.setInPageCallback into Page.addBinding (#236)
This seems to be a much better name which is actually used for a similar
purposes in chromium/v8.
2017-08-10 21:44:49 -07:00
Pavel Feldman
3a7741a172 Introduce page.emulate convenience wrapper (#231)
This patch:
- introduces Page.emulate convenience wrapper
- adds a screenshot-fullPage.js example
2017-08-10 18:42:30 -07:00
JoelEinbinder
0a55345060 Test touch emulation more completely (#235) 2017-08-10 18:25:56 -07:00
Andrey Lushnikov
e1d8a3781e Rename Page.navigate into Page.goto (#232)
Fixes #228.
2017-08-10 00:02:10 -07:00
Andrey Lushnikov
fe06c896eb Roll chromium to 492629 (#230)
This patch
- rolls chromium to 492629
- migrates connection establishing to use browser target. This migration means
  that now we have a single websocket connection to browser (implemented
  in Connection class). A connection to a particular target is
  incapsulated in a new Session class.
2017-08-09 16:14:00 -07:00
Andrey Lushnikov
34b0095c10 Make interception work with redirects (#218)
This patch:
- changes interception API so that it better aligns with what we'd like to see
  in #121
- fixes the issue with redirect interception

Fixes #217.
2017-08-07 17:48:52 -07:00
Andrey Lushnikov
e1c5b8d244 Aborted network requests should use protocol's "Failed" status (#215)
This patch starts using "Failed" command for request interception instead of
"Aborted".

The "Aborted" status also has a side-effect of cancelling the navigation, so
there will be no error on the page and form puppeteer's standpoint, the navigation
will never complete.
2017-08-07 15:13:17 -07:00
Andrey Lushnikov
7888a37a3d Page.pdf should accept case-insensetive page format (#211) 2017-08-04 01:01:10 -07:00
Andrey Lushnikov
af2b790227 Add test to reproduce handing touch events (#208)
References #161.
2017-08-03 22:51:51 -07:00
Andrey Lushnikov
13327f3303 Add test which reproduces the hanging mouse.click() behavior (#207)
This patch adds a disabled test to reproduce the mouse.click() hanging
bug.

References #206.
2017-08-03 22:10:06 -07:00
Andrey Lushnikov
2d650d0301 Introduce CHROME env variable for tests (#205)
This patch adds `CHROME` env variable which allows to
specify custom executable path to run tests.
2017-08-03 21:38:55 -07:00
Andrey Lushnikov
a4c0d58204 Fix tests in non-headless mode (#203)
The chrome-devtools://devtools/bundled/inspector.html is a perfectly
valid url in headful chromium, so we should pick another one for test
to work (and for the navigation inside the test to fail).
2017-08-03 19:11:41 -07:00
Andrey Lushnikov
d8dd6ea140 Add more tests to cover Page.pdf() method (#196) (#200)
This patch adds sanity tests to make sure Page.pdf() produces
sane results.

Fixes #166.
2017-08-03 14:10:52 -07:00
Andrey Lushnikov
fc22b5d888 Revert "Add more tests to cover Page.pdf() method (#196)" (#199)
This reverts commit 403362eaf1.
2017-08-03 13:42:38 -07:00
Andrey Lushnikov
403362eaf1 Add more tests to cover Page.pdf() method (#196)
This patch adds sanity tests to make sure Page.pdf() produces
sane results.

Fixes #166.
2017-08-03 13:37:20 -07:00
Andrey Lushnikov
31224392bb Kill page.$ and page.$$ functions (#197)
page.$ and page.$$ conflicts with our ideas about element handles.

This patch removes functions in favor of future implementation.

References #111
2017-08-03 13:35:31 -07:00
Andrey Lushnikov
6c163122bf Update default viewport size to be 800px by 600px (#194)
This patch updates default viewport size to be 800px by 600px.
This comes handy when observing scripts running in non-headless mode.
2017-08-02 15:47:00 -07:00
JoelEinbinder
984e011cf0 Move path option into tracing.start (#192)
This consolidates all the tracing options in the tracing.start()
2017-08-02 15:41:05 -07:00
JoelEinbinder
315c388f4d Enable dialog tests (#193)
* Enable dialog tests
closes #13

* dismiss dialog test
2017-08-02 15:32:20 -07:00
JoelEinbinder
c32df08e4c Update jsdoc from Tracing patch (#189)
This fixes the comments @a1ph on #181 about JSDOC.
2017-08-02 15:02:41 -07:00
Andrey Lushnikov
3f0306a4d0 [tests] Await all page.evaluate() calls in all tests (#186)
If the evaluation is not awaited for, then the page might be
closed before it's finished, and an exception will be thrown.
2017-08-02 13:24:34 -07:00
Andrey Lushnikov
dbe8660082 Roll chromium to r491334 (#187)
This patch rolls chromium to r491334

This revision includes @pavelfeldman changes to make dialogs
work in headless.
2017-08-02 13:12:44 -07:00
JoelEinbinder
5d647bf1fa Use const a lot fo places (#188) 2017-08-02 12:06:47 -07:00
JoelEinbinder
bd72e40e73 Introduce page.tracing (#181)
This patch introduces page.tracing, which allows to start and stop
trace recording for a page. The trace could be then saved to file.
2017-08-02 10:45:11 -07:00
Andrey Lushnikov
a43c315214 Implement browser 'ignoreHTTPSErrors' option. (#177)
This patch implements Browser 'ignoreHTTPSErrors' option.

Fixes #84.
2017-08-01 15:17:57 -07:00
JoelEinbinder
2acfec0989 Slow mode (#173)
This patch introduces a `slowMo` browser option which
slows down puppeteer operations. Comes handy for debugging
scripts.
2017-07-31 18:47:56 -07:00
JoelEinbinder
bfc40b2ee6 Specify repeat property on repeated keypresses (#172)
This patch implements 'autoRepeat' functionality for `keyboard.down`.
With this patch, the subsequent calls to `keyboard.down` would generate
an event with 'autoRepeat` flag set to true.

Closes #157
2017-07-31 12:05:46 -07:00
Andrey Lushnikov
445dce46f6 response.text() should wait for request to finish
This patch makes sure that request.text() doesn't try
to fetch response body from the backend until the request is
actually finished (finished or failed).
2017-07-29 18:48:30 -07:00
Andrey Lushnikov
67f4264162 Never sent 'requestfinished' event without passing actual request
It turns out we're not receiving 'Network.requestWillBeSent' event
for every requestId.

This patch makes sure we don't dispatch `requestfinished` and
`requestfailed` events without passing actual request.

References #168
2017-07-29 18:34:47 -07:00
Andrey Lushnikov
bd767002bb Fix navigation to about:blank
This patch fixes navigation to about:blank url.
2017-07-28 16:44:51 -07:00
JoelEinbinder
26d97bbe3e Fix injectfile test on Windows (#162) 2017-07-28 11:48:41 -07:00
JoelEinbinder
8780fcb662 Event coverage and debugging (#160)
This patch introduces event coverage for DEBUG module and 
API coverage.

Closes #50.
2017-07-28 11:45:05 -07:00
Andrey Lushnikov
91785d97f3 Add missing LICENSE headers to the source files
This patch:
- adds missing LICENSE headers to the source files
- set's puppeteer version to 0.1.0
- set's repository field in package.json
2017-07-28 01:09:26 -07:00
Andrey Lushnikov
d26e2399f2 Resolve paths against CWD in page.uploadFile() method
This patch:
- teaches page.uploadFile() to resolve given file paths against
  current working directory. This aligns paths handling with all the
  other methods
- moves page.uploadFile() under Frame
- changes test to use relative path for file upload
2017-07-28 00:06:57 -07:00
Andrey Lushnikov
ba37a4f82d Get rid of Body class
The Body class was inlined in the Request and Response classes.
This patch:
- removes the Body class
- adds Request.postData public property
- adds Response.buffer(), Response.text() and Response.json() methods

Fixes #106.
2017-07-27 23:11:24 -07:00
Andrey Lushnikov
b9e3cce5fd Fail navigation when main resource fails to load
This patch fails navigation when the main resource fails to load.

Fixes #148.
2017-07-27 17:54:39 -07:00
Andrey Lushnikov
f1a4598cc1 Clarify coverage report 2017-07-27 17:32:02 -07:00
Andrey Lushnikov
d1f2c01db5 Attempt to fix tests to not throw targetClosed exception
The exception happens due to some of the tests initiating
operation and not waiting for the operation to finish.
2017-07-27 17:20:02 -07:00
Andrey Lushnikov
bd898b7f56 Implement function as a part of a page.waitFor shortcut
This patch adds a function as a possible argument to
page.waitFor shortcut.

Fixes #91.
2017-07-27 17:09:28 -07:00
Andrey Lushnikov
ff5ed1c738 Implement page.waitForFunction method
The page.waitForFunction method allows to wait for a general predicate.
The predicate will be continiously polled for in page, until
it either returns true or the timeout happens.

The polling parameter could be one of the following:
- 'raf' - to poll on every animation frame
- 'mutation' - to poll on every dom mutation
- <number> - to poll every X milliseconds

References #91
2017-07-27 16:45:14 -07:00
Andrey Lushnikov
47a0366b16 Fix typo in coverage
This patch:
- fixes typo in coverage
- enables coverage on Travis CI

References #50.
2017-07-27 16:35:17 -07:00
JoelEinbinder
a2e0d27fb6 Implement public API coverage
This patch:
- implements a basic public API coverage based on 'helper.tracePublicAPI' methods
- adds `npm run coverage` command which reports coverage after running all of the unit tests

References #50.
2017-07-27 16:16:37 -07:00
Andrey Lushnikov
c26d2c8271 Remove redundant methods from Body class
This patch:
- removes Body.arrayBuffer. This method is redundant since there's
  already a Body.buffer() method
- removes Body.bodyUsed getter.

References #106
2017-07-27 14:43:14 -07:00
Andrey Lushnikov
e39d8602b0 Rename page.setHTTPHeaders into page.setExtraHTTPHeaders
This patch:
- renames page.setHTTPHeaders into page.setExtraHTTPHeaders
- starts using Map instead of Object to align with other headers
  arguments

Fixes #112.
2017-07-27 12:51:18 -07:00
JoelEinbinder
bbde8fd1c2 page.evaluate takes a string in addition to function (#135)
This patch improves on page.evaluate to accept a string.
The string can have a trailing '//# sourceURL=' comment which would
name the evaluation to make stacks beautiful.

In order to make sourceURL comments possible, this patch:
- removes wrapping of the client function into `Promise.resolve()`
- stops passing `awaitPromise` parameter to `Runtime.evaluate`
- starts to await promise via the `Runtime.awaitPromise` if the return type of the evaluation
  is promise

closes #118
2017-07-27 12:23:41 -07:00
JoelEinbinder
8870aaee17 Disable and enable some tests based on whether chrome is headless (#154)
This patch:
- Runs dialog test in headful mode only
- Runs pdf test in headless mode only
- Improves on documentation to outline these shortcomings
2017-07-27 11:44:18 -07:00
JoelEinbinder
70f77c1981 add tests for Frame class
This patch:
- improves documentation of frame.name()
- adds a test for frame.name()
- adds a test for frame.parent()

References #50.
2017-07-27 11:36:03 -07:00
JoelEinbinder
dd1459b733 Browser version test (#152)
This patch adds a test for Browser.version()

References #50.
2017-07-27 11:32:58 -07:00
JoelEinbinder
9363a15cce Small fixes to jsdoc (#140)
References #65.
2017-07-26 09:58:03 -07:00
Pavel Feldman
eca0d7fe53 Simplify keyboard modifiers handling (#129) 2017-07-25 14:35:03 -07:00
Pavel Feldman
0007809305 Move inner evaluate helpers into frame class. (#128) 2017-07-25 14:30:04 -07:00
JoelEinbinder
877f06aacb Remove userAgent getter (#126)
If needed, the user agent could be fetched via querying
`navigator.userAgent` in page.

Closes #110
2017-07-25 02:01:10 -07:00
Andrey Lushnikov
d4c5aee5a8 Rename page.evaluateOnInitialized into page.evaluateOnNewDocument
This patch renames page.evaluateOnInitialized into
page.evaluateOnNewDocument to better align with the protocol and with
what the method is actually doing.

Fixes #119.
2017-07-25 00:17:56 -07:00
Andrey Lushnikov
efa33c37b3 Remove page.httpHeaders() getter
Remove the page.httpHeaders() method since it always returns the last
value of page.setHTTPHeaders().

References #115.
2017-07-24 23:35:10 -07:00
Andrey Lushnikov
91cbe561ba Roll chromium to r488994
This patch rolls chromium past the r488926 which prevents chromium from
going to sleep on Mac OS X.

Fixes #100.
2017-07-24 18:28:34 -07:00
JoelEinbinder
a24cec20f9 Fix clicking offscreen horizontally (#105)
This patch starts clicking in the center of the element area which is
visible in a viewport.

Fixes #103.
2017-07-24 16:14:32 -07:00
Andrey Lushnikov
0a3125434e Refactor Frame.waitForSelector method
Refactor Frame.waitForSelector to make room for Frame.waitForFunction
implementation.
This patch:
- removes AwaitedElement class which proved to be confusing, and
  introduces a more straight-forward WaitTask.
- refactors the mutation observer to return true in case of successful
  waiting or false in case of timeout.

References #91
2017-07-24 10:07:43 -07:00
JoelEinbinder
fdaaa2c0e6 Inject file with sourceURL (#102)
This patch starts adding a sourceURL trailing comment to make stack traces
readable.
2017-07-23 09:56:35 -07:00
Andrey Lushnikov
c4904b4e10 Do not leak event listeners on navigation
This patch:
- introduces helper.addEventListener/helper.removeEventListeners
  to simplify event management
- moves NavigatorWatchdog over to the helper.addEventListener to
  stop leaking event listeners
2017-07-22 17:46:39 -07:00
Andrey Lushnikov
5757bc18f2 Rename 'maxTime' option of Page.navigate into 'timeout'
The motivation behind this rename is to name all the 'timeout' options
across methods similarly.

References #39.
2017-07-22 16:32:57 -07:00
JoelEinbinder
98ee35655f Mouse (#101)
This patch:
- adds Mouse class which holds mouse state and implements mouse primitives,
such as moving, button down and button up.
- implements high-level mouse api, such as `page.click` and `page.hover`.

References #40, References #89
2017-07-21 20:29:31 -07:00
JoelEinbinder
eb2cb67b0e Remove keyboard.type and keyboard.press (#98)
This patch removes keyboard.type and keyboard.press methods. The motivation
behind this is to keep only low-level API in the `keyboard` namespace.
2017-07-21 20:00:09 -07:00
Andrey Lushnikov
794f9bb82a [doclint] move all the lint checks in the lint.js
This patch cleans up Documentation.js file by moving all the checks
into the lint.js

Drive-by: fix the Page.waitFor test.
2017-07-21 14:21:17 -07:00
Andrey Lushnikov
dc032b42b9 Introduce polymorphic page.waitFor method
This patch:
- introduces page.waitForSelector to wait for the selector to appear
- introduces polymorphic page.waitFor method, which accepts
either string (and in this case is a shortcut for page.waitForSelector)
or number (and in this case it's a promisified timeout).

References #91.
2017-07-21 12:54:04 -07:00
Andrey Lushnikov
1f954fa7ed Implement timeout option for page.waitFor
This patch implements timeout option for page.waitFor. The function
will throw if the selector doesn't appear during timeout milliseconds
of waittime.

References #89, #91.
2017-07-21 12:07:48 -07:00
Andrey Lushnikov
aba61de905 Make helper.getExceptionMessage synchronous
We now have description of an exception, no need for a roundtrip
to the backend.
2017-07-21 11:57:25 -07:00
Andrey Lushnikov
52de75742b Implement visible option for Page.waitFor method
This patch adds a 'visible' option to the Page.waitFor method, making
it possible to wait for the element to become actually visible.

References #89, #91.
2017-07-21 10:01:19 -07:00
Andrey Lushnikov
139b9e9b6d Get rid of page.emulate() / page.emulatedDevices() methods
This patch:
- gets rid of `page.emulate` and `page.emulatedDevices`
  methods. Instead, it is suggested to use `page.setViewport()`
  and `page.setUserAgent()` methods.
- moves DeviceDescriptors to the top level of the puppeteer so that
  it is convenient to require them.
- improves on documentation to describe the suggested emulation
  approach.

References #88.
2017-07-20 23:53:06 -07:00
Andrey Lushnikov
ffc5a8ae4f Add missing test asset 2017-07-20 16:05:17 -07:00
Andrey Lushnikov
f5215b2ed0 Restart browser once in a while during unit testing
The unit testing on mac is unreliable, making the last tests to hang.
This seems to be related to the upstream bug: crbug.com/741689

For now, this patch restarts the browser in the middle of unit testing.
This should be reverted once the upstream is fixed.

References #100.
2017-07-20 15:29:09 -07:00
Andrey Lushnikov
42edc3108a [emulation] fix touch emulation
This patch:
- fixes touch emulation
- adds tests to cover basic Page.emulate

References #88.
2017-07-20 12:22:34 -07:00
Andrey Lushnikov
e33a8f818c Support HEADLESS env variable for unit tests
This patch makes it possible to run unit tests in non-headless
mode with the following command:

```
HEADLESS=false npm run unit
```
2017-07-19 19:36:22 -07:00
Andrey Lushnikov
a63a0198de Implement waitFor which survives navigation (#99)
This patch implements page.waitFor method which survives navigation.

References #89.
2017-07-19 19:04:51 -07:00
JoelEinbinder
febd747c5b Inroduce page.press (#96)
This patch:
- introduces page.press() method
- adds more input tests

References #89
2017-07-19 14:43:07 -07:00
JoelEinbinder
71f8c76f04 Rename keyboard.hold and release to up and down (#95)
References #89
2017-07-19 14:27:01 -07:00
Andrey Lushnikov
21af495b65 Move screenshot task chain in Browser
Currently, it's impossible to do screenshots in parallel.
This patch:
- makes all screenshot tasks sequential inside one browser
- starts activating target before taking screenshot
- adds a test to make sure it's possible to take screenshots across
  tabs
- starts waiting for a proper page closing after each test. This might
  finally solve the ECONNRESET issues in tests.

References #89
2017-07-19 14:15:16 -07:00
Andrey Lushnikov
2e94f9f67b Resolve pending callbacks when connection closes
This patch:
- resolves pending callbacks in connection once it gets closed
- fixes one of Page.screenshot() tests to wait for all the screenshots
- starts handling ECONNRESET error in test server to avoid throwing with no
  good reason
2017-07-19 01:00:02 -07:00
Pavel Feldman
f154d537f7 Introduce page.goBack/page.goForward (#93)
This patch introduces page.goBack/page.goForward methods
to navigate the page history.

References #89.
2017-07-18 19:11:37 -07:00
Pavel Feldman
98c3894c84 Introduce Page.waitForNavigation (#94)
This patch introduces Page.waitForNavigation which allows to wait
for render-initiated navigation.

This patch also does a nice refactoring, replacing Navigator with NavigatorWatcher which
is not a part of a page state.

References #89
2017-07-18 18:54:24 -07:00
JoelEinbinder
28265fc313 Fix frame.waitFor to work with complex selectors (#90)
The frame.waitFor didn't account for multi-level selectors.
This patch fixes this, executing document.querySelector on every DOM mutation.
2017-07-18 02:10:02 -07:00
Andrey Lushnikov
6c1c3a0c45 Return remote object description for unserializable objects
This patch starts handling unserializable objects by returning their
description.

Fixes #86.
2017-07-18 01:34:43 -07:00
Andrey Lushnikov
2ca08b032b Rename Page's 'consolemessage' event into 'console'
This patch:
- renames 'consolemessage' event into 'console'
- improves on 'console' event documentation

References #39.
2017-07-17 20:38:11 -07:00
Andrey Lushnikov
1a97d8b3c2 Teach 'consolemessage' event to send all the arguments
This patch fixes 'consolemessage' event so that it passes
over all the arguments of console API call.
2017-07-17 20:22:45 -07:00
JoelEinbinder
117a128b42 Introduce Page.$ and Page.$$ (#75)
This patch introduces Page.$ and Page.$$ methods which are
aliases for `document.querySelector` and `document.querySelectorAll`. 

Fixes #78.
2017-07-17 18:56:56 -07:00
JoelEinbinder
bf7698e8f8 Intorduce Page.keyboard (#74)
Introduce page.keyboard to provide low-level access to the keyboard.
2017-07-17 18:49:52 -07:00