# note: if you change the version here change it in the other .tcl # files as well package provide tclwebtest 1.0 package require http # this version contains the speedup fixes of gustaf neumann (up to more than 60 times faster) # - much more efficient caption code # - let tcl compile expressions, ifs, etc... namespace eval ::tclwebtest:: { namespace export do_request reset_session debug response cookies assert assertion_failed link form field translate_entities known_bug } # try to import the base64 package, fake it if we can't if {![catch {package require base64}]} { set ::tclwebtest::base64_encode ::base64::encode } else { set ::tclwebtest::base64_encode ::tclwebtest::fake_base64_encode } # set static variables namespace eval ::tclwebtest:: { # do we print debugging msgs that are in this file? variable DEBUG_LIB_P 0 variable VERSION "0.9" # shell the html checker tidy be invoked on each result page? variable TIDY 0 # follow dirty redirects by default? variable FOLLOWEQUIV 1 # default identation variable LOG_MESSAGE_INDENTATION "" # user agent strings set user_agent_dict(original) "tclwebtest/$VERSION" set user_agent_dict(opera70) "Mozilla/4.0 (compatible; MSIE 6.0; MSIE 5.5; Windows NT 5.1) Opera 7.0 \[en\]" set user_agent_dict(netscape70) "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)" set user_agent_dict(elinks04) "ELinks (0.4pre24.CVS; Linux 2.4.19-4GB i686; 145x55)" set user_agent_dict(mozilla09) "Mozilla/5.0 (X11; U; Linux i686; en-US; rv:0.9.8) Gecko/20020204" # initial value set user_agent $user_agent_dict(original) } ::tclwebtest::ad_proc -public ::tclwebtest::init {} {

Defines all tclwebtest wide variables that carry state information and sets them to a default value. To be used upon initialization and to reset the variables. The variables will exist in the ::tclwebtest:: namespace.

When running tclwebtest normally from a shell don't need to call this explicitely, it will be called during the first call of package require. If you are running tclwebtest from within AOLServer however you need to call ::tclwebtest::init explicitely in the beginning of your session because the connection thread might have been reused from the previous connection and could contain old variable values.

} { # Important: every variable command here must also set a value, # otherwise reset won't work as expected and cause ugly and subtle # failures. variable url "" variable http_status "" variable body "" variable body_without_comments "" # All visible text of this page variable text "" # A list that contains alternating cookie name and value. Value is # a list by itself, containing array key and value pairs. This is # different from the format of the other lists, e.g. links variable cookies [list] # A list of http headers returned by the last http request. Format # is suitable to initialize an array with. Same as the 'meta' # element from the tcl http command. variable headers [list] # A list that contains lists, each with two elements, url_prefix # and value. Value is the string which has to be injected 'as-is' # in the http request header Authorization. Usually the list will # be sorted alphabetically decreasing variable http_authentication [list] # This is an ugly global hack to avoid polluting do_request's # parameters, when it's not empty, it will be injected in the # http request and deleted. See do_request's 401 treatement. variable http_auth_string "" # not implemented yet variable framed_p "" variable frames_name "" variable frames_body "" # has the corresponding proc already been called after the current # request? variable links_extracted_p 0 # A list of lists. The inner list contains a key/value list # prepared for array set; with those keys: full, url, content variable links [list] # has the corresponding proc already been called after the current # request? variable forms_extracted_p 0 # Again a list of lists, where the inner list is an array # list. Keys: full, action, method, content, fields. Fields is a # list of array lists, that contains those keys: full, name, type, # value, caption, choices # caption is what appears before the field, e.g. "First name:" # choices is a list of possible values when field is of type # select, radio or checkbox. For checkbox it will always contain # an empty string and the value of the "value" attribute variable forms [list] # Indices of the current active form and field. variable current_form "" variable current_field "" variable field_modified_p 0 variable current_link "" variable referer_policy 1 variable forged_referer "" } namespace eval ::tclwebtest:: { init } ::tclwebtest::ad_proc -public ::tclwebtest::user_agent_id { id } { By default tclwebtest identifies itself as "tclwebtest/$VERSION". With this command you can change the the agent string. This is useful to test web sites which send different HTML depending on the user browser, or you need to fake an identity because some webmaster decided to restrict access to his page to a subset of the popular browsers. You can find a list of common user agent strings at: http://www.pgts.com.au/pgtsj/pgtsj0208c.html @param id Indicate the user agent you want to set, which should be a string. There are a few shortcuts built into tclwebtest, you can set id to opera70, msie60, netscape70, mozilla09 and elinks04. Use list to retrieve a pair list in the form shortcut, agent string with the currently available shortcuts. Use original if you want to set back the default tclwebtest string. Example:
    log "Showing available builtin user agent strings:"
    array set agents [user_agent_id list]
    foreach id [array names agents] {
        log "  $id: $agents($id)"
    }
    
    log "Let's fool Google"
    do_request -nocomplain http://www.google.com/search?q=linux
    user_agent_id "super mozilla like browser"
    do_request http://www.google.com/search?q=linux
    
} { if { $id eq "list" } { return [array get ::tclwebtest::user_agent_dict] } elseif { [catch { set ::tclwebtest::user_agent $::tclwebtest::user_agent_dict($id) }] } { set ::tclwebtest::user_agent $id } log "User agent string switched to '$::tclwebtest::user_agent'" } ::tclwebtest::ad_proc -deprecated -public ::tclwebtest::reset_session { } { Used to reset the session to a pristine state, as if there had been no use of tclwebtest at all. Example:
    do_request "file://$TESTHOME/select.html"
    assert { [string length [response text]] > 0 }

    reset_session

    assert { [response text] eq "" }

    debug "l: [string length [response text]]"
    

Deprecated, use ::tclwebtest::init instead.

@see ::tclwebtest::init } { namespace eval ::tclwebtest:: init } ::tclwebtest::ad_proc -public ::tclwebtest::debug { -lib:boolean msg } { Emit the message to stdout, even if logging output is redirected to a file. Intended to be used when writing tests. Its only advantage over using puts is that it does not have to be deleted when writing the test is finished. However, if the variable ::tclwebtest::in_memory exists (through the previous execution of test_run), this procedure will route it's message through ::tclwebtest::log, thus allowing all output to be redirected to a memory string. } { set output "" if { $lib_p } { # debugging message from the tclwebtest code itself if { [info exists ::tclwebtest::DEBUG_LIB_P] && $::tclwebtest::DEBUG_LIB_P } { set output "DEBUG LIB: $msg" } } else { # normal debugging message. TODO implement a -debug switch, # currently it's always on set output "DEBUG: $msg" } if { $output ne "" } { variable in_memory if { [info exists in_memory] && $in_memory != 0} { log "$output" } else { puts "$output" } } } ::tclwebtest::ad_proc -public ::tclwebtest::log { msg } { Log msg to log_channel. If the variable ::tclwebtest::in_memory exists (through the previous execution of ::tclwebtest::test_run), ::tclwebtest::log_channel will be treated like a string instead of a file object (which defaults to stdout). } { variable log_channel variable in_memory set charmap [list "\n" "\n$::tclwebtest::LOG_MESSAGE_INDENTATION"] set msg [string map $charmap $msg] if { [info exists in_memory] && $in_memory != 0 } { if { $msg ne "" } { append log_channel "$::tclwebtest::LOG_MESSAGE_INDENTATION$msg\n" } return } if { ![info exists log_channel] || $log_channel eq "" } { set log_channel stdout } puts $log_channel "$::tclwebtest::LOG_MESSAGE_INDENTATION$msg" } # --------------------------------------------------------------------------- # Begin of link procs. # TODO implement current_link similar to work like current_field # (esp. when no further arguments are given, e.g. like this "link # follow") ::tclwebtest::ad_proc -public ::tclwebtest::link { command args } { Search for the first link in the extracted links that matches the given criteria and return an index-value pair list with it's contents, which can be converted to an array if you want to extract specific attributes manually. If there is no link that matches, throws assertion_failed. Example of retrieving links, which you could use with do_request to create a simple web crawler:
    link reset_current
    while { ![catch { link find -next } ] } {
        debug "found a link: [link get_text] to [link get_url]"
    }
    
@param command Specify one of the commands: find, follow, all, reset_current, current, get_*.
find
Find and return the first link that matches args (or the first link if no args are given). Valid modifiers for args:
~c (default). content, the text between the <a></a> tags
~u url (content of the href attribute)
~f full html source
Additionally, you can use the following switches in args:
-index
???
-next
Used alone to loop throught available links. If current_link is the last link of the page, it will throw assertion_failed unless you are using the switch -fail too.
-fail
Negates the outcome, e.g. if a link is searched and not found, it won't throw assertion_failed, if the search was negated, then assertion_failed would be thrown, etc.

Matching syntax

The syntax of the matching functionality is inspired by the filter function in the Mutt mailclient. A list of arguments can be given as search criteria. When an argument is the "~" followed by a single character it acts as modifier that determines in which data field the following argument has to match. There is always a default data field.

So e.g. a hyperlink has the data fields content (the text between the <a>...</a> tags), url (the href attribute) and full (the full html source of that link). content is the default data field, so you can search for a link that contains some text like that:

    link find "sometext"
    
If you are looking for a specific url add the ~u modifier:
    link find ~u "/some/url"
    
Several search criteria are automatically concatenated with AND. So you can search for:
    link find "sometext" ~u "/some/url" "someothertext" ~f "<a[^>]+class=\"?someclass\"?[^>]>"
    
All those attributes have to match - the link must contain "sometext" AND "someothertext" in its text, point to the specified url AND must have a class attribute. Note that e.g. the class attribute is not parsed in a specified data field, so it has to be retrieved by searching the full html source of the field (but at least its possible to search for it at all). Search criterias can contain a "!" to specify that the following argument must NOT match. "!" can optionally be prepended or followed by a "~" modifier. For example:
    link find "sometext" ! ~u "/but/not/this/url"
    
The matching will always be done with "regexp -nocase". I wonder if case sensitive matching will ever be necessary for website testing. See the proc find_in_array_list for the matching implementation.

Currently search arguments are appended at the end of the command and get parsed into the args parameter of the proc. This allows for the maybe more convenient syntax:

    link find "some text with spaces" ~u "/some/url"
    
as opposed to putting everything in a seperate list:
    link find { "some text with spaces" ~u "/some/url" }
    
Commands supporting this behaviour are find, form and field.
follow
this is the same like doing link find (which means that you can use a regular expression as parameter), and then do_request with the previous result. Example:
    link follow "Download"
    link follow "Back to contents"
    link follow ~u {em[[:alpha:]]+sa}
    
Note that after this command you can get the current URL with response url, useful if you followed a link by text and you want to store/verify the url tclwebtest chose.
all
Returns all links. Example:
    assert { [llength [link all]] == 3 }

    do_request some_url
    
    foreach { data } [link all] {
        array set one_link $data
        log "found link, dumping contents"
        foreach { key } [array names one_link] {
            log "  $key: $one_link($key)"
        }
    }
    
Note that this example is different from the previous one which retrieves all links, in that here you can't use the get command to extract only the text of a link, because link works like a state machine, and after the all command it points at the last link of the current page.

The only keys you can rely on being available are url, content and full. You might see more when you log all the packaged data, but these are for internal use and you shouldn't use them.

Besides getting everything at once, you can use the familiar expression sintax to get only specific links. Also, this command accepts the switch -onlyurl, which will return only the available urls instead of the whole information of each link which was shown with the previous example. You can also add the -absolute switch to convert all links to absolute urls. The following example connects to Slashdot and then retrieves all links with the text content "Read more" which have the word article in their urls:

    do_request http://slashdot.org/
    foreach url [link all -onlyurl {read more} ~u article] {
        log "$url"
    }
    
reset_current
This will delete the internal pointer current_link, which will behave as if the current page was just loaded and no link search had been done. Usefull to go back from link find -next.
current
Returns the currently selected link. If no link is selected, the first one will be returned, or an assertion thrown if there are no links at all.
get_*
Returns the specified attribute of a link. Example:
    debug "found a link: [link get_text]"
    assert { [link get_full] == "<a href=\"mailto:tils@tils.net\">Tilmann Singer</a>" }
    
} { extract_links_if_necessary if { [regexp {get_(.+)} $command match attribute_name] } { eval link_get $attribute_name $args } else { eval link_$command $args } } ::tclwebtest::ad_proc -private ::tclwebtest::link_find { -index:boolean -next:boolean -fail:boolean args } { find a link with the given attributes or the first one and return the full list or the index. } { # We need a way to loop through all links.This assumes # that somehow current_link will be set to -1 for a full # search through all links and is somewhat different to # the field behaviour if { $next_p } { # find the next link that matches after the # current_link # fail if current_link points to the last link already if { [expr {$::tclwebtest::current_link + 1}] >= [llength $::tclwebtest::links] } { if { $fail_p } { return } else { assertion_failed "No more links, thus \"link find -next\" failed. " } } incr ::tclwebtest::current_link set offset $::tclwebtest::current_link debug -lib "next is true, offset is $offset" } else { # find the first that matches set offset 0 } # do something if called without args if { [llength $args] == 0 } { if { [llength $::tclwebtest::links] == 0 } { if { $fail_p } { return } else { assertion_failed "There are no links" } } else { if { !$next_p } { # "link find" called without -next and without # arguments if { $::tclwebtest::current_link == -1 } { # set to first link set ::tclwebtest::current_link 0 } } if {$index_p} { return $::tclwebtest::current_link } else { return [lindex $::tclwebtest::links $::tclwebtest::current_link] } } } set list_to_search [lrange $::tclwebtest::links $offset end] set found_idx [find_in_array_list -index $list_to_search [list ~c content ~u url ~f full] $args] if { $found_idx eq "" } { if { $fail_p } { return } else { assertion_failed "No link found that matches '$args'" } } else { set found_idx [expr {$found_idx + $offset}] set ::tclwebtest::current_link $found_idx if { $fail_p } { # found something while -fail was set assertion_failed "'link find -fail $args' did not expect to find a link, yet it found this one: [lindex $::tclwebtest::links $found_idx]" } else { if {$index_p} { return $found_idx } else { return [lindex $::tclwebtest::links $found_idx] } } } } ::tclwebtest::ad_proc -private ::tclwebtest::link_reset_current { } { see "field find -next" above } { set ::tclwebtest::current_link -1 } ::tclwebtest::ad_proc -private ::tclwebtest::link_follow args { just a shortcut for "do_request [link find args]" } { set evalstr "do_request \[link get_url $args\]" debug -lib "following a link by: $evalstr" eval $evalstr } ::tclwebtest::ad_proc -private ::tclwebtest::link_current { } { } { if { $::tclwebtest::current_link == -1 } { # set to the first link. will scream if there are no # links link find debug -lib "resetting current_link" } debug -lib "current_link is $::tclwebtest::current_link" return [lindex $::tclwebtest::links $::tclwebtest::current_link] } ::tclwebtest::ad_proc -private ::tclwebtest::link_all { -fail:boolean -onlyurl:boolean -absolute:boolean args } { @see link } { # do something if called without args if { [llength $args] == 0 } { if { [llength $::tclwebtest::links] == 0 } { if { $fail_p } { return } else { assertion_failed "There are no links" } } else { set value $::tclwebtest::links } } else { set value [find_in_array_list -return_matches $::tclwebtest::links [list ~c content ~u url ~f full] $args] } if { $absolute_p } { set counter -1 foreach item $value { incr counter array set a_link $item set a_link(url) [post_process_url [absolute_link $a_link(url)]] set value [lreplace $value $counter $counter [array get a_link]] } } if { $onlyurl_p } { set temp [list] foreach item $value { array set a_link $item lappend temp $a_link(url) } set value $temp } return $value } ::tclwebtest::ad_proc -private ::tclwebtest::link_get { attribute_name args } { return the specified attribute of a link. TODO write a selftest for this } { # it's called content but some might want to call it text, # which sounds better if { $attribute_name eq "text" } { set attribute_name "content" } if { [llength $args] > 0 } { eval "link find $args" } array set a_link [link current] if { [lsearch [array names a_link] $attribute_name] == -1 } { error "In link $command: $attribute_name not found in array, we only have: [array names a_link]" } return $a_link($attribute_name) } # End of link procs. # --------------------------------------------------------------------------- # The weird command dispatching below is mainly done to preserve the # old api 'assert text -fail' instead of having to switch to 'assert # -fail text' after ad_proc-ifying this, to avoid breaking existing # test files. Peter Marklund suggested a different API: 'assert # ![response_contains ]'. Should consider when # changing other APIs. ::tclwebtest::ad_proc -public ::tclwebtest::assert { args } { Test args, a required boolean expresion (and only one). Throws assertion_failed if it's false. Usually args is a comparison.

Examples:

    assert { $val eq "foo" }
    assert text "Hello world"
    assert full "body>here"
    assert -fail { 0 == 1 }
    assert text -fail "Do not find me"
    assert full -fail "<not"
    
@param text Test an expression against the visible text of the result page. @param full Test an expression against the full HTML source of the result page. @param fail the assertion expects the condition to be false. } { if { [lindex $args 0] eq "body" || [lindex $args 0] eq "full" } { eval assert_body [lrange $args 1 end] } elseif { [lindex $args 0] eq "text" } { eval assert_text [lrange $args 1 end] } else { # normal condition if { [lindex $args 0] eq "-fail" } { set fail_p 1 set condition [lindex $args 1] } else { set fail_p 0 set condition [lindex $args 0] } # Sets the variable _assert_p in the calling # context. Don't know how else i could evaluate a # condition in the same way that if {...} does set to_eval "if \{ $condition \} \{ set _assert_p 1 \} else \{ set _assert_p 0 \}" uplevel $to_eval upvar _assert_p _assert_p if { $_assert_p == $fail_p } { set condition [string trim $condition] set extra [list] # Heuristically parse the condition for all variables # ($a, $b ...), upvar them and append their values to # the message. Cannot do execution of [...] blocks # (would propably have unforeseeable side effects) set condition_to_search $condition regsub -all {\[.*?\]} $condition {} condition_to_search foreach word [split $condition_to_search {$}] { if { $word eq "" || [string first $word $condition_to_search] == 0 } { # the first result from the split is the text # from the beginning to the first $ continue } if {[regexp {(.*?)(\s|$)} $word match varname]} { debug -lib "displaying the value of '$varname'" upvar $varname value if { [string length $value] > 30 } { set value "[string range $value 0 26]..." } lappend extra "\$$varname: $value" } } if {$fail_p} { assertion_failed "Assertion \"$condition\" did not fail, but -fail was given" } else { assertion_failed "Assertion \"$condition\" failed. [join $extra "; "]" } } } } ::tclwebtest::ad_proc -private ::tclwebtest::assert_text { -fail:boolean search_expr } { Test an expression against the visible text of the result page. TODO make a more meaningful message } { if {$fail_p} { assert -fail { [regexp -nocase $search_expr $::tclwebtest::text] } } else { assert { [regexp -nocase $search_expr $::tclwebtest::text] } } } ::tclwebtest::ad_proc -private ::tclwebtest::assert_body { -fail:boolean search_expr } { Test an expression against the full html source of the result page. } { if {$fail_p} { assert -fail { [regexp -nocase $search_expr $::tclwebtest::body] } } else { assert { [regexp -nocase $search_expr $::tclwebtest::body] } } } # from /acs-test-harness/tcl/test-procs.tcl ::tclwebtest::ad_proc -public ::tclwebtest::assertion_failed { assertionMsg } { Usually called by tclwebtest, this procedure will raise an error with the messsage assertionMsg, which will be caught by a test unit and written to the standard output. There will be no error code. } { error "$assertionMsg" "$assertionMsg\n--- end of assertionMsg ---\n" { NONE } } # current - get or set the index of the current form. TODO complain # when called with non-existing command # parameters should be # command # args ::tclwebtest::ad_proc -public ::tclwebtest::form { command args } { tclwebtest keeps an internal pointer, current_form. Operations that do not explicitely specify a target operate on the element of the current pointer. When such an operation is called before a current form or field is set, then the pointer will be set to the first possible value. As a result, the first (or the only) form is always the default. @param command Specify one of the commands: find, submit, current, get_*, all.
find
Set the current_form pointer to the form that matches or to the first form when called without args. Valid modifiers for args:
~c (default).The user viewable text (content) that is between the <form></form> tags.
~a the form's action
~m method - either get or post, in lower case
~n name of the form (also searches id attribute)
~f the full html source of the form
Returns the form as a list suitable for array set. For a deeper explanation of the matching syntax, take a look at the documentation of link find. Examples:
    form find "dropdown"
    form find ~n "form1"
    
submit
Submit the current form. Invokes form find if no current form has previously been set. You can use this without parameters (only the first submit button of the form will be used to build the query) or specify a regular expression to select the submit button you want to use/push. You can also use the search modifiers like with form find. Examples:
    # required to avoid getting forbidden access (403)
    user_agent_id "Custom mozilla"

    # get number of found entries for tclwebtest
    do_request http://www.google.com/
    field fill tclwebtest
    form submit

    # go directly to the first entry
    do_request http://www.google.com/
    field fill tclwebtest
    form submit {feeling lucky}
    
current
Returns the currently selected form. Example:
    # the third form
    form find "firma"
    assert { [form current] == 2 }
    
get_*
Returns the specified attribute of a form. TODO write a selftest for this.
all
Return a list of all forms.
} { extract_forms_if_necessary if { $command ne "find" } { if { ![info exists ::tclwebtest::current_form] || $::tclwebtest::current_form == -1 } { # initialize to the first form form find } if { [set current_form $::tclwebtest::current_form] == -1 } { error "No form found at all" } } eval form_$command $args } ::tclwebtest::ad_proc -private ::tclwebtest::form_find args { find - set currentform to the first form or to a form on the page with specified criteria } { # TODO -next if { [llength $::tclwebtest::forms] == 0 } { # no forms present assertion_failed "No form present" } elseif { [llength $args] == 0 } { # set to first form on this page set ::tclwebtest::current_form 0 return } set found [find_in_array_list -index $::tclwebtest::forms [list ~c content ~a action ~m method ~n name ~f full] $args] if { $found eq "" } { assertion_failed "No form with found with search_string $args" } set ::tclwebtest::current_form $found set ::tclwebtest::current_field 0 set ::tclwebtest::field_modified_p 0 } ::tclwebtest::ad_proc -private ::tclwebtest::form_submit { {args ""} } { Using the current form, it will search for a post/get method and use it to retrieve the next page. Only one submit button will be used, either the first found, or the one specified by args with a regular expression. @param args Regular expression which should match one available submit button. } { array set a_form [form current] set list_to_search [field all] # tricky search: ignore non-submit forms, and search for value instead of content set args "~T submit $args" set found [find_in_array_list -index $list_to_search { ~c value ~T type ~f full ~v value ~n name } $args] if { $found eq "" } { # no submit widget found. we allow to submit anyway since that # is possible in a browser too by hitting return. set submit_name "" set submit_value "" } else { array set a_field $[lindex $list_to_search $found] set submit_name $a_field(name) set submit_value $a_field(value) } # detect method and initialise memory/function pointers if { [string compare -nocase $a_form(method) "post"] == 0 } { set temp [list] set add_value_to_form_query add_value_to_form_post_query set finish_form_query finish_form_post_query } elseif { [string compare -nocase $a_form(method) "get"] == 0 } { set temp "" set add_value_to_form_query add_value_to_form_get_query set finish_form_query finish_form_get_query } else { assertion_failed "Bogus form doesn't have a method?\n$a_form(fields)" } set temp_files [list] # now loop over fields calling the procs which build the query foreach field $a_form(fields) { catch { unset a_field } array set a_field $field if { [string match $a_field(type) "submit"] } { # ignore all submit buttons } elseif { [string match $a_field(type) "select"] } { # a select field - special format of value (it's a # list) foreach select_value $a_field(value) { $add_value_to_form_query temp $a_field(name) $select_value } } elseif { [string match $a_field(type) "checkbox"] } { if { $a_field(value) ne "" } { $add_value_to_form_query temp $a_field(name) $a_field(value) } } elseif {$a_field(type) eq "file"} { if { $a_field(value) ne "" } { $add_value_to_form_query temp_files $a_field(name) $a_field(value) } } else { $add_value_to_form_query temp $a_field(name) $a_field(value) } } # add submit button as last element and perform query $add_value_to_form_query temp $submit_name $submit_value $finish_form_query temp temp_files a_form } ::tclwebtest::ad_proc -private ::tclwebtest::add_value_to_form_post_query { holder_name field_name field_value } { form_submit helper for post query operations. @param holder_name name of the variable holding the temporary memory used to store additional query parameters. @param field_name name of the field to be added to the query. @param field_value value of the field to be added to the query. } { upvar $holder_name holder lappend holder $field_name $field_value } ::tclwebtest::ad_proc -private ::tclwebtest::finish_form_post_query { holder_name file_holder_name form_name } { form_submit helper for finishing post query operations, it will call do_request after building the query correctly from the parameter list holder_name. @param holder_name name of the variable holding the temporary memory used to store all the query parameters. @param form_name name of the array containing all the form fields. } { upvar $holder_name holder $file_holder_name file_holder $form_name a_form debug -lib "POSTING: $holder" do_request -enctype $a_form(enctype) -files $file_holder $a_form(action) $holder } ::tclwebtest::ad_proc -private ::tclwebtest::add_value_to_form_get_query { holder_name field_name field_value } { form_submit helper for get query operations. @param holder_name name of the variable holding the temporary memory used to store additional query parameters. @param field_name name of the field to be added to the query. @param field_value value of the field to be added to the query. } { upvar $holder_name holder append holder &[http::formatQuery $field_name]= append holder [http::formatQuery $field_value] } ::tclwebtest::ad_proc -private ::tclwebtest::finish_form_get_query { holder_name file_holder_name form_name } { form_submit helper for finishing get query operations, it will call do_request after building the query correctly from the parameter list holder_name. @param holder_name name of the variable holding the temporary memory used to store all the query parameters. @param form_name name of the array containing all the form fields. } { upvar $holder_name holder $file_holder_name file_holder $form_name a_form if { [llength $file_holder] > 0 } { error "Trying to submit a form with a file using GET" } set url [absolute_link $a_form(action)] set query_string "$url?[string range $holder 1 end]" debug -lib "FORM GET: $query_string" do_request $query_string } ::tclwebtest::ad_proc -private ::tclwebtest::form_current { -index:boolean } { just return the current form } { if {$index_p} { return $::tclwebtest::current_form } else { return [lindex $::tclwebtest::forms $::tclwebtest::current_form] } } ::tclwebtest::ad_proc -private ::tclwebtest::form_get { attribute_name args } { return the specified attribute of a form. TODO write a selftest for this } { array set a_form [form current] if { [lsearch [array names a_form] $attribute_name] == -1 } { error "In form $command: $attribute_name not found in array, we only have: [array names a_form]" } return $a_form($attribute_name) } ::tclwebtest::ad_proc -private ::tclwebtest::form_all { } { return a list of all forms } { return $::tclwebtest::forms } # --------------------------------------------------------------------------- # Begin field procs # We keep a pointer to the current field. It will initially (after # first access to any form or field) be set to index 0. The command # "field find" and all commands that call it will set that pointer to # the index of the found field. Additionally we keep a flag # field_modified_p, that will be set by all commands that modify the # field value, and unset by a find operation. # TODO add a ~i attribute for the index of the field. ::tclwebtest::ad_proc -public ::tclwebtest::field { command args } { tclwebtest keeps an internal pointer, current_form. Operations that do not explicitely specify a target operate on the element of the current pointer. When such an operation is called before a current field is set, then the pointer will be set to the first possible value. As a result, the first (or the only) field is always the default.

Setting field values (via field fill / check / uncheck / select) does the following: it searches for the first applicable field (e.g. a field check searches a checkbox) starting from the current_field position, sets the value, and then advances the current_field pointer by one. Thus it is possible to handle a form of two text entries and a checkbox using this brief (and hopefully convenient) syntax:

field fill "foo"
field fill "bar" ~n fieldname
field fill -append "baz"
field check
form submit
This assumes that there are two text (or textarea or password) fields, followed by one checkbox. The commands would have to be reordered if the form items were in another order. @param command Specify one of the commands: check / unckeck a single checkbox, check / unckeck a group of checkboxes with common name, current, fill, find, select, multiselect, deselect. get_*,
find
Find the first field that matches args , or the first user modifyable field (e.g. not of type hidden nor submit) when no args are given, and set the current_field pointer to it. Valid modifiers for args:
~c (default). caption
~t the type, can be text, textarea, password, hidden, radio, checkbox, select or submit
~v current value
~n name
~f full html source
Returns the field as a list suitable for array set. For a deeper explanation of the matching syntax, take a look at the documentation of link find. Examples:
    field find "title"
    field find "Start Time 6:00"
    field find "End Time"
    field find "Description"
    
all
Returns a list that contains all data of all fields.
current
Used to set or get the value of the currently selected form field. This is a pointer value, with 0 being the first field, 1 the second, etc. When no ?value? is given, the pointer is returned, otherwise set. Usually you will prefer to use field find args to select form fields.
fill
Fill args into a field that takes text as input - either text, textarea or password. It also moves the current_field pointer to the next field. By default tclwebtest will replace the content of the field, but you can specify the optional boolean parameter -append to append the selected text to the value the form currently contains. Example:
field fill -append { and some more options...}
If you specify a pattern the first field that matches will be filled instead of the current_field. The pattern goes at the end of the command, after the new value. Example:
field fill "bar" ~n fieldname
If the field is of type file upload then you can enter a filename relative to the test location, e.g. 'some_file.txt' or the full path like this: '/path/to/some_file.txt', and tclwebtest will try to upload the specified file.
check / uncheck
Check or uncheck the currently selected checkbox field.
select
Select a value of a radio button or a select field. If it is a multiple select then deselect the others. You can select a specific index (starting from 0) of the select field with the -index parameter. If you want to know the options of the select, use field get_choices. This returns a list of pairs in the form value/text, where value is equal to text if the HTML of the <option> tag doesn't have a value attribute.
multiselect
Add one or more values to the current selection of a multiple select field. If a value is not found in the selection box, you will get an assertion error indicating which values are available, and which were the ones you asked and weren't found.
deselect
Delete the selection of a multiple select field. (Not possible with drop-downs and radio buttons)
get_*
Returns the specified attribute of a form field. Typical attributes you can retrieve from most fields are name, value, type and full. The availability of these and other attributes depends on the HTML code used by the field.
} { extract_forms_if_necessary if { $::tclwebtest::current_form == -1 } { form find } if { [regexp {^get_(.+)$} $command match attribute_name] } { # some command like 'field get_value' eval field_get $attribute_name $args } else { # one of the other field commands eval field_$command $args } } ;# end of field ::tclwebtest::ad_proc -private ::tclwebtest::field_fill { -append:boolean value args } { @see field } { set form [lindex $::tclwebtest::forms $::tclwebtest::current_form] array set a_form $form find_field_of_type { text textarea password file } $args set cf $::tclwebtest::current_field ;# just a shortcut set field [lindex $a_form(fields) $cf] array set a_field $field if { $append_p } { set a_field(value) "$a_field(value)$value" } else { set a_field(value) $value } set field [array get a_field] set a_form(fields) [lreplace $a_form(fields) $cf $cf $field] set form [array get a_form] set ::tclwebtest::forms [lreplace $::tclwebtest::forms $::tclwebtest::current_form $::tclwebtest::current_form $form] set ::tclwebtest::field_modified_p 1 } ::tclwebtest::ad_proc -private ::tclwebtest::field_fill_hidden { value args } { Fill the specified text in the first hidden field. This just updates the field(value), not the actual html code in field(full) and is a copy of fill above } { set form [lindex $::tclwebtest::forms $::tclwebtest::current_form] array set a_form $form find_field_of_type { hidden } $args set cf $::tclwebtest::current_field ;# just a shortcut set field [lindex $a_form(fields) $cf] array set a_field $field set a_field(value) $value set field [array get a_field] set a_form(fields) [lreplace $a_form(fields) $cf $cf $field] set form [array get a_form] set ::tclwebtest::forms [lreplace $::tclwebtest::forms $::tclwebtest::current_form $::tclwebtest::current_form $form] set ::tclwebtest::field_modified_p 1 } ::tclwebtest::ad_proc -private ::tclwebtest::field_select { -index:boolean search_arg args } { field select ?-index? value ?search_args ...? To be used with radio buttons or select fields. Select the entry that contains the specified text in its caption or the nth entry by specifying -index and a number as value (starting from 0). If the field is a select with multiple selections, then the new value will replace previously selected ones. } { set form [lindex $::tclwebtest::forms $::tclwebtest::current_form] array set a_form $form find_field_of_type { radio select } $args array set a_field [lindex $a_form(fields) $::tclwebtest::current_field] if { $index_p } { # was called with -index if { $search_arg > [expr {[llength $a_field(choices)] - 1}] } { assertion_failed "This field (name: $a_field(name)) cannot be set to index $value. Its choices are: $a_field(choices)" } set a_field(value) [list [lindex [lindex $a_field(choices) $search_arg] 0]] } else { # not called with -index, e.g. an expression is given # for the desired caption to set # TODO we might allow for search args such as { ~v # some-explicit-value } instead of just searching in # caption debug -lib "SEARCHING FOR: $search_arg" set found_p 0 foreach choice $a_field(choices) { set looped_value [lindex $choice 0] set looped_caption [lindex $choice 1] if { [regexp -nocase $search_arg $looped_caption] } { set found_p 1 debug -lib "FOUND: $choice" set a_field(value) [list $looped_value] break } } if {!$found_p} { assertion_failed "This field has no choice $search_arg. It's only offerings are: $a_field(choices)" } } replace_current_field [array get a_field] set ::tclwebtest::field_modified_p 1 } ;# end of select ::tclwebtest::ad_proc -private ::tclwebtest::field_select2 { args } { field select2 ~i index ~c caption ~v value ~d ID assumes you've found the form and the field first } { set form [lindex $::tclwebtest::forms $::tclwebtest::current_form] array set a_form $form array set a_field [lindex $a_form(fields) $::tclwebtest::current_field] set found [find_in_array_list -index $a_field(choices2) { ~c caption ~v value ~i index ~d id } $args] if {$found eq ""} { assertion_failed "Select option not found. Args were: $search_arg $args" } else { set a_field(value) [list [lindex [lindex $a_field(choices) $found] 0]] } replace_current_field [array get a_field] set ::tclwebtest::field_modified_p 1 } ;# end of select ::tclwebtest::ad_proc -private ::tclwebtest::field_multiselect { value_list args } { } { set form [lindex $::tclwebtest::forms $::tclwebtest::current_form] array set a_form $form find_field_of_type select [concat ~m 1 $args] array set a_field [lindex $a_form(fields) $::tclwebtest::current_field] set missing_choices "" foreach subex $value_list { # loop through all search args (when this is not a # multiple select then there will only be one) debug -lib "SEARCHING FOR SUBEX: $subex" set found_p 0 foreach choice $a_field(choices) { set looped_value [lindex $choice 0] set looped_caption [lindex $choice 1] if { [regexp -nocase $subex $looped_caption] } { set found_p 1 if { $a_field(type) eq "select" && $a_field(multiple_p) } { # a multiple selection - add the found # value to the existing value list lappend a_field(value) $looped_value } else { # no multiple selection allowed - just set the value set a_field(value) [list $looped_value] } break } } ;# next choice if {!$found_p} { if { $missing_choices eq "" } { set missing_choices "`$subex'" } else { append missing_choices ", `$subex'" } } } ;# next subex if { $missing_choices ne "" } { assertion_failed "This field doesn't contain the following choices: $missing_choices. It's only offerings are: $a_field(choices)" } replace_current_field [array get a_field] set ::tclwebtest::field_modified_p 1 } ;# end of multiselect ::tclwebtest::ad_proc -private ::tclwebtest::field_deselect args { } { # clear a multiple select field. TODO find_field_of_type select [concat ~m 1 $args] set form [lindex $::tclwebtest::forms $::tclwebtest::current_form] array set a_form $form array set a_field [field current] if { ! ($a_field(type) eq "select" && $a_field(multiple_p)) } { assertion_failed "You cannot deselect the field $a_field(name) because it is not a multiple select field" } set a_field(value) [list] replace_current_field [array get a_field] set ::tclwebtest::field_modified_p 1 } ;# end of deselect ::tclwebtest::ad_proc -private ::tclwebtest::field_check args { field check ?search_args ...? Check a checkbox } { find_field_of_type checkbox $args set form [lindex $::tclwebtest::forms $::tclwebtest::current_form] array set a_form $form array set a_field [field current] if { $a_field(type) ne "checkbox" } { assertion_failed "This field is not a checkbox" } set a_field(value) [lindex $a_field(choices) 1] replace_current_field [array get a_field] set ::tclwebtest::field_modified_p 1 } ;# end of select ::tclwebtest::ad_proc -private ::tclwebtest::field_check_multiple { checkbox_name checkbox_values } { Loop over a group of checkboxes with a given name and select those with a value matching a list of values. @checkbox_name The name of the checkboxes to check @checkbox_values A list of values that indicate which checkboxes to check @autor Peter Marklund } { while { 1 } { if { [catch {field find -next ~n $checkbox_name ~t checkbox}] } { # No more checkboxes break } array set current_field [::tclwebtest::field_current] set checkbox_value [lindex $current_field(choices) 1] if { [lsearch -exact $checkbox_values $checkbox_value] != -1 } { field check } else { # field_find -next will give us the current field eternally as # long as it hasn't been modified so increment current field manually incr ::tclwebtest::current_field } } } ::tclwebtest::ad_proc -private ::tclwebtest::field_uncheck args { } { # opposite of field check find_field_of_type checkbox $args set form [lindex $::tclwebtest::forms $::tclwebtest::current_form] array set a_form $form array set a_field [field current] if { $a_field(type) ne "checkbox" } { assertion_failed "This field is not a checkbox" } set a_field(value) [lindex $a_field(choices) 0] replace_current_field [array get a_field] set ::tclwebtest::field_modified_p 1 } ;# end of uncheck ::tclwebtest::ad_proc -private ::tclwebtest::field_get { attribute_name args } { return the specified attribute of a field. TODO write a selftest for this } { if { [llength $args] > 0 } { eval "field find $args" } array set a_field [field current] if { [lsearch [array names a_field] $attribute_name] == -1 } { error "$attribute_name not found in array, we only have: [array names a_field]" } return $a_field($attribute_name) } ::tclwebtest::ad_proc -private ::tclwebtest::field_current { -index:boolean {new_index ""} } { @see field } { set form [lindex $::tclwebtest::forms $::tclwebtest::current_form] array set a_form $form if { $::tclwebtest::current_field == -1 } { field find } if { $new_index ne "" } { # set the current field if { [expr {[llength $a_form(fields)] - 1}] < $new_index } { assertion_failed "field current: Cannot set current field to index $new_index, field count is: [llength $a_form(fields)]" } set ::tclwebtest::current_field $new_index set ::tclwebtest::field_modified_p 0 } else { # return the current field if {$index_p} { return $::tclwebtest::current_field } else { return [lindex $a_form(fields) $::tclwebtest::current_field] } } } ::tclwebtest::ad_proc -private ::tclwebtest::field_all { } { @see field } { set form [lindex $::tclwebtest::forms $::tclwebtest::current_form] array set a_form $form return $a_form(fields) } ::tclwebtest::ad_proc -private ::tclwebtest::field_find { -next:boolean args } { @see field } { # TODO implement -fail set form [lindex $::tclwebtest::forms $::tclwebtest::current_form] array set a_form $form if { [llength $a_form(fields)] == 0 } { assertion_failed "there are no fields in the current form" } if { [llength $args] == 0 } { # no search criteria - set args to search for any # fillable field. Hidden fields cannot be manipulated # and thus cannot be found either. They are just # there. set args [list ~t (text|textarea|password|checkbox|radio|select)] } if { $next_p } { # search starts from the current field or from the one # after if the current has been modified already if { $::tclwebtest::field_modified_p } { set offset [expr {$::tclwebtest::current_field + 1}] } else { set offset $::tclwebtest::current_field } set list_to_search [lrange $a_form(fields) $offset end] } else { # search starts from the beginning set list_to_search $a_form(fields) set offset 0 } set found [find_in_array_list -index $list_to_search { ~c caption ~f full ~t type ~v value ~n name ~m multiple_p ~i index ~d id ~C choices } $args] if { $found eq "" } { assertion_failed "Field not found. Args were: $args" } else { set ::tclwebtest::current_field [expr {$offset + $found}] set ::tclwebtest::field_modified_p 0 return [lindex $a_form(fields) $::tclwebtest::current_field] } } ::tclwebtest::ad_proc -private ::tclwebtest::replace_current_field { new_field } { @see field } { set cfo $::tclwebtest::current_form set cfi $::tclwebtest::current_field array set a_form [lindex $::tclwebtest::forms $cfo] set a_form(fields) [lreplace $a_form(fields) $cfi $cfi $new_field] set ::tclwebtest::forms [lreplace $::tclwebtest::forms $cfo $cfo [array get a_form]] } ::tclwebtest::ad_proc -private ::tclwebtest::find_field_of_type { types search_args } { Set the current field accordingly (used by the "field xxx" procs) } { if { [string trim [join $search_args]] == "" } { set to_eval "field find -next ~t ([join $types "|"])" } else { set to_eval "field find ~t ([join $types "|"]) $search_args" } eval $to_eval } # End of field procs # --------------------------------------------------------------------------- # ------------------------------------------------------------------ # ---------------------- Cookies ----------------------------------- ::tclwebtest::ad_proc -public ::tclwebtest::cookies { command {cookies_to_add ""} } { @param command Specify one of the commands: clientvalue, all, persistent, set, clear.
clientvalue
A string that is the concatenation of all cookies that this http client wants to set. E.g. the value of the "Cookie: " http header.
all
Return all cookies that are currently used in this session.
persistent
Return all persistent cookies, e.g. those that the browser would store on the harddisk, in a name/value paired list.
set
Set the currently used cookies of this session to args.Typically used to test persistent cookies, for example those of a permanent login. args must be formatted like the output of cookies persistent.
clear
Clears all the cookies from memory.
} { switch $command { clientvalue { set result_list [list] foreach { name cookie } $::tclwebtest::cookies { catch { unset a_cookie } array set a_cookie $cookie lappend result_list "$name=$a_cookie(value)" } return [join $result_list "; "] } all { # return all cookies return $::tclwebtest::cookies } persistent { # return persistent cookies set result [list] foreach { name cookie } $::tclwebtest::cookies { catch { unset a_cookie } array set a_cookie $cookie if { [info exists a_cookie(persistent_p)] && $a_cookie(persistent_p) } { lappend result $name $cookie } } return $result } set { # TODO maybe let this only be called at the beginning of a # session. # TODO only correctly deals with the output of "cookie # all" or "cookie persistent". Does not even throw an # error if input is not correct. It should check and set # the few needed values, so that cookies can be manually # written in test cases too. if { [llength $cookies_to_add] == 0 } { log "'cookies set' was called with an empty list, so I assume we should clear the cookies" cookies clear return } array set a_cookies $::tclwebtest::cookies foreach { name cookie } $cookies_to_add { set a_cookies($name) $cookie } set ::tclwebtest::cookies [array get a_cookies] } clear { set ::tclwebtest::cookies [list] } default { error "the command \"cookies $command\" does not exist" } } } ::tclwebtest::ad_proc -private ::tclwebtest::scan_cookie_expiration_time { time_string } { Used to parse the different cookie time values thrown by servers. It tries iteratively different formats until one of them is parsed correctly, and then returns the cookie expiration time in seconds since the starting epoch. If unable to parse the time, assertion_failed is raised to abort the request. } { if { [catch { set ret [clock scan $time_string]}] == 0 } { return $ret } # Try stripping trailing 'garbage' from time_string set ttime [string range $time_string 0 [string first $time_string ";"]] if { [catch { set ret [clock scan $ttime]}] == 0 } { return $ret } assertion_failed "scan_cookie_expiration_time unable to parse cookie time '$time_string'" } ::tclwebtest::ad_proc -private ::tclwebtest::set_cookie { set_cookie_string } { Parses the value of the http "Set-cookie: " header into the global cookies list. Doesn't do anything with the path argument, e.g. if it is not / than this proc will behave wrongly. Also it currently depends on the Expires= information to be parsable by the tcl command clock scan. If that is not the case then it will throw an error. } { # Cookies spec. is here: # http://wp.netscape.com/newsref/std/cookie_spec.html Some # examples of actual cookies seen on the web follow: # 'cookietest=1; expires=Mon, 09-May-2033 04:18:36 GMT; path=/' # # --atp@piskorski.com, 2003/05/09 00:22 EDT array set a_cookies $::tclwebtest::cookies if ![regexp {([^=]+)=([^;]*)} $set_cookie_string match name value] { error "I did not understand this Cookie value: \"$set_cookie_string\". Please improve me." } set a_cookie(full) $set_cookie_string if { [regexp -nocase {expires\s*=\s*([^;]*)} $set_cookie_string match expires] } { set a_cookie(persistent_p) 1 set a_cookie(expires) $expires set expires_seconds [scan_cookie_expiration_time $expires] if { $expires_seconds > [clock seconds] } { # A persistent cookie that wants to be set set a_cookie(persistent_p) 1 } else { # A cookie that wants to be unset (either persistent or # not) catch { unset a_cookies($name) } set ::tclwebtest::cookies [array get a_cookies] return } #debug -lib "expires: $expires" } else { # A non-persistent cookie set a_cookie(persistent_p) 0 set a_cookie(expires) "" } set a_cookie(name) $name ;# Note: the name is also stored in the outer cookie array set a_cookie(value) $value #debug -lib "string: $cookie_string" #debug -lib "name: $cookie_name, value: $cookie_value" # append this cookie set a_cookies($name) [array get a_cookie] set ::tclwebtest::cookies [array get a_cookies] } # --- end of Cookies ----------------------------------------------- # ------------------------------------------------------------------ ::tclwebtest::ad_proc -private ::tclwebtest::add_referer_header { header_list_name previous_url } { Given the name of a list containing the headers of the next http request, adds to this list previous_url as referer depending on the value of previous_url and ::tclwebtest::referer_policy. } { if { $previous_url ne "" } { upvar $header_list_name headers switch -- $::tclwebtest::referer_policy { 0 { # do nothing } 1 { # send correct referer lappend headers "Referer" $previous_url debug -lib "using referer $previous_url" } 2 { # send fake referer if { $::tclwebtest::forged_referer ne "" } { lappend headers "Referer" $::tclwebtest::forged_referer } } } } } ::tclwebtest::ad_proc -public ::tclwebtest::referer { url_or_type } { Use this command to modify the referer url field which will be sent by tclwebtest in the following http requests, or change the policy. Calling reset_session will reset the policy to the default value (1). @param url_or_type can be any of the following values:
0 (numerical) or emtpy string ("")
Using one of these values will desactivate sending the referer field in subsequent http requests.
1 (numerical)
tclwebtest will send the real referer expected to be sent by a usual browser: if you are making the first request, no referer will be sent, otherwise, the location you are coming from will be sent as referer. Note that this applies to both link and do_request commands. You will have to call reset_session to clean the old referer url, as well as other session variables like cookies. This is the default value.
string
Any url you may want to specify, which means that all the following requests will use it in the referer http field. Calling reset_session will eliminate the forged url.
} { if { $url_or_type == 1 } { set ::tclwebtest::referer_policy 1 } elseif { $url_or_type == 0 || $url_or_type eq "" } { set ::tclwebtest::referer_policy 0 } else { set ::tclwebtest::referer_policy 2 set ::tclwebtest::forged_referer $url_or_type } } ::tclwebtest::ad_proc -public ::tclwebtest::open_browser { html {tmp_file ""} } { Save the given html result in a temporary file and launch a browser. Intended to be inserted into a test while writing it to give a more thorough feedback than with debug.

Currently not satisfying since some referenced files such as images and stylesheets are required for a pleasing display. Maybe it is sufficient to parse the html for all those links, download the files and save them in a temporary directory, and replace them in the parsed html for local references. } { global tcl_platform if { $tmp_file eq "" } { set tmp_file "/tmp/test_lib_tcl/tmp.html" } file mkdir [file dirname $tmp_file] set file [open $tmp_file w] #puts $file $html close $file switch $tcl_platform(platform) { "unix" { exec "mozilla" "file://$tmp_file" "&" } "windows" { eval "exec [auto_execok start] [list "file://$tmp_file"] &" } } } ::tclwebtest::ad_proc -public ::tclwebtest::do_request { {-followequiv:boolean 0} {-nocomplain:boolean 0} {-nocomplain_list {}} {-noredirect:boolean 0} {-files {}} {-enctype "application/x-www-form-urlencoded"} url { query_key_values "" } } { Do an http request and store everything in the current session. Returns the URL it has reached, which can be different from your request if there were redirections.

Expects a sane reply from the server, e.g. no status "500 Internal Server Error" or 404's and throws an assertion_failed otherwise. Here you have a list of possible http error codes.

If you have problems with URL's that are on another port than at the standard http port 80, look at the tcl bug #452217 at sourceforge, related to duplicate Host: headers. @param url the url to be requested. Examples: http://openacs.org/register/, /foo/bar/some_page.html, some_page.html, file:///tmp/my_test_file.html. @param query_key_values list of query elements (in the format: key value key value ...) for a POST request. If not set then the request will be a GET, otherwise a POST. @param followequiv follow redirects that are specified inside the html code with the http-equiv tag (for weird websites) @param nocomplain don't fail on 404's and other errors @param nocomplain_list a more specialised version of -nocomplain, this parameter accepts a list of the error codes you explicitly want to ignore, and it will fail with those error codes not included in your list. If -nocomplain is present in the request, this parameter is ignored completely. Example:

    do_request -nocomplain {301 401} url
    
@param noredirect don't follow server redirections. This is useful for example if you want to verify the redirection chain of steps of a specific site and see the values of the cookies set at every new step. Also useful if you wan't to make sure the url you are getting is the one you requested. Example:
    set original http://slashdot.org/
    set new [do_request $original]
    assert { $new == $original }
    
} { variable regexp_script_before_html variable regexp_http_equiv_redirect if {$nocomplain_p} { set nocomplain_option "-nocomplain -nocomplain_list {$nocomplain_list}" } else { set nocomplain_option " -nocomplain_list {$nocomplain_list}" } # As a global option. Added this here because i needed it after a # "form submit", but thats suboptimal. if { $::tclwebtest::FOLLOWEQUIV } { set followequiv_p 1 } if {$followequiv_p} { set followequiv_option "-followequiv" } else { set followequiv_option "" } # reset all parts of the session that have to be reset set ::tclwebtest::links_extracted_p 0 set ::tclwebtest::links [list] set ::tclwebtest::forms_extracted_p 0 set ::tclwebtest::forms [list] set ::tclwebtest::current_form -1 # remove any bookmark reference from the end of the url TODO write # a selftest regsub {#[^#]*$} $url {} url # for emacs if {1} {} #" set url [post_process_url [absolute_link $url]] set previous_url $::tclwebtest::url set ::tclwebtest::url $url log "--- do_request for $url" set final_url $url # Kludge to deal with file:// urls. TODO implement this cleaner # into the do_request proc if { [string match "file://*" $url] } { return [do_request_file $url] } if { [string match "https://*" $url] } { require_https_support } ::http::config -useragent $::tclwebtest::user_agent set headers [list] if { [llength $::tclwebtest::cookies] > 0 } { lappend headers "Cookie" [cookies clientvalue] } # adding the referer http field if needed add_referer_header headers $previous_url # detect if we have to inject an http authorization set already_tried_http_authorization 0 if { $::tclwebtest::http_auth_string ne "" } { lappend headers "Authorization" $::tclwebtest::http_auth_string set ::tclwebtest::http_auth_string "" set already_tried_http_authorization 1 } set geturl_command [list ::http::geturl $url -headers $headers] # Decide if we add parameters for a POST operation if {$enctype eq "multipart/form-data"} { set query_content {} set boundary "-----NEXT_PART_[clock seconds].[pid]" foreach { elmname filename } $files { set fd [open $filename r] fconfigure $fd -translation binary if { [catch { read $fd [file size $filename] } data] } { return -code error $data } close $fd append query_content "--$boundary\r\nContent-Disposition: form-data;\ name=\"$elmname\"; filename=\"[file tail $filename]\"\r\n\r\n$data\r\n" } foreach { elmname data } $query_key_values { append query_content "--$boundary\r\nContent-Disposition: form-data;\ name=\"$elmname\"\r\n\r\n$data\r\n" } append query_content "--${boundary}" lappend geturl_command -type "$enctype; boundary=$boundary" lappend geturl_command -query $query_content } elseif { $query_key_values ne "" } { set query_content [eval "::http::formatQuery $query_key_values"] lappend geturl_command -query $query_content } set token [eval $geturl_command] upvar #0 $token http_result regexp { (\d\d\d) } $http_result(http) full_match http_status #[string range $http_result(http) 0 2] log "http status: >>$http_status<<" set ::tclwebtest::http_status $http_status set ::tclwebtest::headers $http_result(meta) # check if we received a Set-Cookie header, add to cookies if # necessary foreach { header header_value } $::tclwebtest::headers { if { [string compare -nocase $header "set-cookie"] == 0 } { set_cookie $header_value } } set ::tclwebtest::body $http_result(body) set ::tclwebtest::body_without_comments [strip_html_comments $http_result(body)] set ::tclwebtest::text [translate_entities [util_striphtml $::tclwebtest::body_without_comments]] set failure_treatement "debug -lib $nocomplain_p if { !$nocomplain_p && [lsearch $nocomplain_list $http_status] == -1 } { assertion_failed \"do_request did not return a page. HTTP status is $http_status\" } else { log \"Bad http answer ignored due to -nocomplain\" }" set avoid_tidy_p 0 # is it a redirect ? if { $http_status == "302" || $http_status == "301" || $http_status == "307" } { set avoid_tidy_p 1 for { set i 0 } { $i < [llength $::tclwebtest::headers] } { incr i 2 } { if { [string match -nocase [lindex $::tclwebtest::headers $i] "location"] } { set location [translate_entities [string trim [lindex $::tclwebtest::headers [expr {$i+1}]]]] break } } if { $location eq "" } { # when location is null after redirection, get relative directory set location "./" } if { $http_status == "301" } { if { $nocomplain_p || [lsearch $nocomplain_list "301"] != -1 } { log "Attention! Redirection 301 was ignored, but please update your test unit, it's a bug!" } else { assertion_failed "Permanent redirection (301) are considered a test unit bug\nUse -nocomplain if needed." } } if {$noredirect_p} { # debugging log "ignoring redirect to: $location" set final_url $location } else { log "following a redirect to: $location" eval [build_do_request_retry redirect] } } elseif { [regexp -expanded -nocase $regexp_script_before_html $http_result(body) match location] } { # a very silly form of redirect, with a } # TODO should consider that this is inside a comment variable regexp_http_equiv_redirect {]+)"?\s*/?>} }