From b1c33e58bfa433dd3ed358a0ce03d64388f6601e Mon Sep 17 00:00:00 2001 From: wirepair Date: Mon, 8 Feb 2016 09:13:16 +0900 Subject: [PATCH] add more documentation --- README.md | 33 ++++++++++++++++++++++++++++----- tab.go | 32 +++++++++++++++++++------------- tab_test.go | 14 +++++++++----- 3 files changed, 56 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 749e969..9f97f26 100644 --- a/README.md +++ b/README.md @@ -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"). @@ -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. @@ -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. \ No newline at end of file +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. \ No newline at end of file diff --git a/tab.go b/tab.go index 4c38665..ec3c991 100644 --- a/tab.go +++ b/tab.go @@ -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 @@ -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 @@ -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 } @@ -175,6 +177,7 @@ func open(target *gcd.ChromeTarget) (*Tab, error) { return t, nil } +// close our exitch. func (t *Tab) close() { if !t.IsShuttingDown() { close(t.exitCh) @@ -182,6 +185,7 @@ func (t *Tab) close() { t.setShutdownState(true) } +// Is the tab shutting down? func (t *Tab) IsShuttingDown() bool { if flag, ok := t.shutdown.Load().(bool); ok { return flag @@ -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 } @@ -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 @@ -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, "" @@ -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) @@ -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) @@ -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() @@ -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 { @@ -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() @@ -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) { @@ -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 @@ -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{} @@ -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() { diff --git a/tab_test.go b/tab_test.go index 35208a8..c17b125 100644 --- a/tab_test.go +++ b/tab_test.go @@ -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) } @@ -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") @@ -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() { @@ -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() @@ -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 { @@ -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") @@ -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!" {