Skip to content

Commit

Permalink
add more documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
wirepair committed Feb 8, 2016
1 parent 90d6900 commit b1c33e5
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 23 deletions.
33 changes: 28 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ See the [examples](https://github.com/wirepair/autogcd/tree/master/examples) or
The chrome debugger service uses internal nodeIds for identifying unique elements/nodes in the DOM. In most cases you will not need to use this identifier directly, however if you plan on calling gcdapi related features you will probably need it. The most common example of when you'll need them is for getting access to a nested #document element inside of an iframe. To run query selectors on nested documents, the nodeId of the iframe #document must be known.

### Elements
The Chrome Debugger by nature is far more asynchronous than WebDriver. It is possible to work with elements even though the debugger has not yet notified us of their existence. To deal with this, Elements can be in multiple states; Ready, NotReady or Invalid. Only certain features are available when an Element is in a Ready state. If an Element is Invalid, it should no longer be used and references to it should be
discarded.
The Chrome Debugger by nature is far more asynchronous than WebDriver. It is possible to work with elements even though the debugger has not yet notified us of their existence. To deal with this, Elements can be in multiple states; Ready, NotReady or Invalid. Only certain features are available when an Element is in a Ready state. If an Element is Invalid, it should no longer be used and references to it should be discarded.

### Frames
If you need to search elements (by id or by a selector) of a frame's #document, you'll need to get an Element reference that is the iframe's #document. This can be done by doing a tab.GetElementsBySelector("iframe"), iterating over the results and calling element.GetFrameDocumentNodeId(). This will return the internal document node id which you can then pass to tab.GetDocumentElementsBySelector(iframeDocNodeId, "#whatever").
Expand All @@ -38,9 +37,9 @@ Finally, you can use the tab.WaitFor method, which takes a ConditionalFunc type
For example/simple ConditionalFuncs see the [conditionals.go](https://github.com/wirepair/autogcd/tree/master/conditionals.go) source. Of course you can use whatever you want as long as it matches the ConditionalFunc signature.

### Navigation Errors
Unlike webdriver, we can actually determine if navigation fails, *at least in chromium. After tab.Navigate(url), calling tab.DidNavigationFail() will return a true/false return value along with a string of the failure type if one did occur. It is strongly recommended you pass the following flags: --test-type, --ignore-certificate-errors on start up of autogcd if you wish to ignore certificate errors.
Unlike WebDriver, we can determine if navigation fails, *at least in chromium. After tab.Navigate(url), calling tab.DidNavigationFail() will return a true/false return value along with a string of the failure type if one did occur. It is strongly recommended you pass the following flags: --test-type, --ignore-certificate-errors on start up of autogcd if you wish to ignore certificate errors.

\* This does not appear to work in chrome at least on windows.
\* This does not appear to work in chrome in windows or osx.

### Input
Only a limited set of input functions have been implemented. Clicking and sending keys. You can use Element.SendKeys() or send the keys to whatever is focused by using Tab.SendKeys(). Only Enter ("\n"), Tab ("\t") and Backspace ("\b") were implemented, to use them, simply add them to your SendKeys argument Element.SendKeys("enter text hit enter\n") where \n will cause the enter key to be pressed.
Expand Down Expand Up @@ -76,4 +75,28 @@ As mentioned in the Elements section, Chrome Debugger Protocol is fully asynchro
This package has been *heavily* tested in the real world. It was used to scan the top 1 million websites from Alexa. I found numerous goroutine leaks that have been subsequently fixed. After running my scan I no longer see any leaks. It should also be completely safe to kill the browser at any point and not have any runaway go routines since I have channels waiting for close messages at any point a channel is sending or receiving.

## Reporting Bugs & Requesting Features
Found a bug? Great! Tell me what version of chrome/chromium you are using and how to reproduce and I'll get to it when I can. Keep in mind this is a side project for me. Same goes for new features. Patches obviously welcome.
Found a bug? Great! Tell me what version of chrome/chromium you are using and how to reproduce and I'll get to it when I can. Keep in mind this is a side project for me. Same goes for new features. Patches obviously welcome.


## License
The MIT License (MIT)

Copyright (c) 2016 isaac dawson

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
32 changes: 19 additions & 13 deletions tab.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/*
The MIT License (MIT)
Copyright (c) 2015 isaac dawson
Copyright (c) 2016 isaac dawson
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down Expand Up @@ -83,6 +83,7 @@ func (e *TimeoutErr) Error() string {
return "Timed out " + e.Message
}

// Internal response function type
type GcdResponseFunc func(target *gcd.ChromeTarget, payload []byte)

// Called when the tab crashes or the inspector was disconnected
Expand Down Expand Up @@ -154,6 +155,7 @@ func open(target *gcd.ChromeTarget) (*Tab, error) {
t.stableAfter = 300 * time.Millisecond // default 300 ms for considering the DOM stable
t.domChangeHandler = nil

// enable various debugger services
if _, err := t.Page.Enable(); err != nil {
return nil, err
}
Expand All @@ -175,13 +177,15 @@ func open(target *gcd.ChromeTarget) (*Tab, error) {
return t, nil
}

// close our exitch.
func (t *Tab) close() {
if !t.IsShuttingDown() {
close(t.exitCh)
}
t.setShutdownState(true)
}

// Is the tab shutting down?
func (t *Tab) IsShuttingDown() bool {
if flag, ok := t.shutdown.Load().(bool); ok {
return flag
Expand All @@ -198,6 +202,7 @@ func (t *Tab) Debug(enabled bool) {
t.debug = enabled
}

// Set the disconnected handler so caller can trap when the debugger was disconnected/crashed.
func (t *Tab) SetDisconnectedHandler(handlerFn TabDisconnectedHandler) {
t.disconnectedHandler = handlerFn
}
Expand Down Expand Up @@ -232,6 +237,7 @@ func (t *Tab) setIsNavigating(set bool) {
t.isNavigatingFlag.Store(set)
}

// Are we currently navigating?
func (t *Tab) IsNavigating() bool {
if flag, ok := t.isNavigatingFlag.Load().(bool); ok {
return flag
Expand Down Expand Up @@ -306,10 +312,10 @@ func (t *Tab) Navigate(url string) (string, error) {
return frameId, err
}

// An undocumented method of determining if chrome failed to load
// An undocumented method of determining if chromium failed to load
// a page due to DNS or connection timeouts.
func (t *Tab) DidNavigationFail() (bool, string) {
// if loadTimeData doesn't exist, we get a js error, basically meaning no error occurred.
// if loadTimeData doesn't exist, or we get a js error, this means no error occurred.
rro, err := t.EvaluateScript("loadTimeData.data_.errorCode")
if err != nil {
return false, ""
Expand Down Expand Up @@ -351,7 +357,7 @@ func (t *Tab) NavigationHistory() (int, []*gcdapi.PageNavigationEntry, error) {
return t.Page.GetNavigationHistory()
}

// Reloads the page injecting evalScript to run on load. set Ignore cache to true
// Reloads the page injecting evalScript to run on load. set ignoreCache to true
// to have it act like ctrl+f5.
func (t *Tab) Reload(ignoreCache bool, evalScript string) error {
_, err := t.Page.Reload(ignoreCache, evalScript)
Expand Down Expand Up @@ -408,7 +414,7 @@ func (t *Tab) BackEntry() (*gcdapi.PageNavigationEntry, error) {
return nil, &InvalidNavigationErr{Message: "Unable to navigate backward as we are on the first navigation entry"}
}

// Calls a function every rate until conditionFn returns true or timeout occurs.
// Calls a function every tick until conditionFn returns true or timeout occurs.
func (t *Tab) WaitFor(rate, timeout time.Duration, conditionFn ConditionalFunc) error {
rateTicker := time.NewTicker(rate)
timeoutTimer := time.NewTimer(timeout)
Expand Down Expand Up @@ -479,7 +485,7 @@ func (t *Tab) GetScriptSource(scriptId string) (string, error) {
}

// Gets the top document and updates our list of elements DO NOT CALL DOM.GetDocument after
// the page has loaded, it creates a new nodeId and all functions that look up elements (QuerySelector)
// the page has loaded, it creates new nodeIds and all functions that look up elements (QuerySelector)
// will fail.
func (t *Tab) getDocument() (*Element, error) {
doc, err := t.DOM.GetDocument()
Expand All @@ -495,6 +501,7 @@ func (t *Tab) getDocument() (*Element, error) {
return eleDoc, nil
}

// Returns the top level document element for this tab.
func (t *Tab) GetDocument() (*Element, error) {
docEle, ok := t.getElement(t.GetTopNodeId())
if !ok {
Expand Down Expand Up @@ -624,7 +631,7 @@ func (t *Tab) GetDocumentElementsBySelector(docNodeId int, selector string) ([]*
return elements, nil
}

// Returns the documents source, as visible, if docId is 0, returns top document source.
// Returns the document's source, as visible, if docId is 0, returns top document source.
func (t *Tab) GetPageSource(docNodeId int) (string, error) {
if docNodeId == 0 {
docNodeId = t.GetTopNodeId()
Expand Down Expand Up @@ -754,9 +761,8 @@ func (t *Tab) pressSystemKey(systemKey string) error {
}

// Injects custom javascript prior to the page loading on all frames. Returns scriptId which
// can be used to remove the script. Since this loads on all frames, if you only want the
// script to interact with the top document, you'll need to do checks in the injected script
// such as testing location.href.
// can be used to remove the script. If you only want the script to interact with the top
// document, you'll need to do checks in the injected script such as testing location.href.
//
// Alternatively, you can use Tab.EvaluateScript to only work on the global context.
func (t *Tab) InjectScriptOnLoad(scriptSource string) (string, error) {
Expand Down Expand Up @@ -888,7 +894,7 @@ func (t *Tab) StopConsoleMessages(shouldDisable bool) error {
return err
}

// Listens to network traffic, either handler can be nil in which case we'll only call the handler defined.
// Listens to network traffic, each handler can be nil in which case we'll only call the handlers defined.
func (t *Tab) GetNetworkTraffic(requestHandlerFn NetworkRequestHandlerFunc, responseHandlerFn NetworkResponseHandlerFunc, finishedHandlerFn NetworkFinishedHandlerFunc) error {
if requestHandlerFn == nil && responseHandlerFn == nil && finishedHandlerFn == nil {
return nil
Expand Down Expand Up @@ -1005,7 +1011,7 @@ func (t *Tab) StopStorageEvents(shouldDisable bool) error {
}

// Set a handler for javascript prompts, most likely you should call tab.Page.HandleJavaScriptDialog(accept bool, msg string)
// to actually handle the prompt, otherwise the tab will be blocked waiting for input and never additional events.
// to actually handle the prompt, otherwise the tab will be blocked waiting for input and never return additional events.
func (t *Tab) SetJavaScriptPromptHandler(promptHandlerFn PromptHandlerFunc) {
t.Subscribe("Page.javascriptDialogOpening", func(target *gcd.ChromeTarget, payload []byte) {
message := &gcdapi.PageJavascriptDialogOpeningEvent{}
Expand Down Expand Up @@ -1058,7 +1064,7 @@ func (t *Tab) subscribeEvents() {
t.subscribeTargetDetached()
}

// listens for NodeChangeEvents and crash events and dispatches them accordingly.
// Listens for NodeChangeEvents and crash events, dispatches them accordingly.
// Calls the user defined domChangeHandler if bound. Updates the lastNodeChangeTime
// to the current time. If the target crashes or is detached, call the disconnectedHandler.
func (t *Tab) listenDebuggerEvents() {
Expand Down
14 changes: 9 additions & 5 deletions tab_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ func TestTabFrameGetPageSource(t *testing.T) {
if err != nil {
t.Fatalf("error getting tab")
}
tab.Debug(true)
//tab.Debug(true)
if _, err := tab.Navigate(testServerAddr + "iframe.html"); err != nil {
t.Fatalf("Error navigating: %s\n", err)
}
Expand Down Expand Up @@ -517,7 +517,7 @@ func TestTabFrameRedirect(t *testing.T) {
t.Fatalf("error getting tab")
}

tab.Debug(true)
//tab.Debug(true)

if _, err := tab.Navigate(testServerAddr + "frame_top.html"); err != nil {
t.Fatalf("error opening first window")
Expand All @@ -543,6 +543,7 @@ func TestTabFrameRedirect(t *testing.T) {
}
ifrDoc, _ := tab.GetElementByNodeId(ifrDocNodeId)

// wait for setTimeout redirect
time.Sleep(4 * time.Second)

if !ifr.IsInvalid() {
Expand Down Expand Up @@ -578,7 +579,11 @@ func TestTabMultiTab(t *testing.T) {
wg.Wait()
}

// This only works in chromium
func TestTabNavigationError(t *testing.T) {
if !strings.Contains(testPath, "chromium") {
return
}
testAuto := testDefaultStartup(t)
defer testAuto.Shutdown()
tab, err := testAuto.NewTab()
Expand Down Expand Up @@ -629,7 +634,7 @@ func TestTabSslError(t *testing.T) {
if err != nil {
t.Fatalf("error getting tab")
}
tab.Debug(true)
//tab.Debug(true)
// Test expired SSL certificate
// "--test-type", "--ignore-certificate-errors", should not return any errors
if _, err := tab.Navigate("https://expired.identrustssl.com/"); err != nil {
Expand Down Expand Up @@ -688,7 +693,7 @@ func TestTabChromeUnhandledCrash(t *testing.T) {
t.Fatalf("error getting tab")
}
timeout := time.NewTimer(time.Second * 10)
tab.Debug(true)
//tab.Debug(true)
go func() {
<-timeout.C
t.Fatalf("timed out waiting for termination event")
Expand All @@ -703,7 +708,6 @@ func TestTabChromeUnhandledCrash(t *testing.T) {
func testMultiNavigateSendKeys(t *testing.T, wg *sync.WaitGroup, tab *Tab) {
var err error
var ele *Element
// sleep for a random ms so we aren't attempting to send all events at the same time

msgHandler := func(callerTab *Tab, message *gcdapi.ConsoleConsoleMessage) {
if message.Text == "zomgs Test!" {
Expand Down

0 comments on commit b1c33e5

Please sign in to comment.