Seaside: Ajax callback, then append server-rendered element? - ajax

My goal is to create a menu item (a span) which, when clicked, does
three things. First, it sets some state on the server (addTurn), then
it creates a new element (renderEmptyTurnOn) on the server and sends
it to the client. Then the client, having received the new element,
appends it to a specific element with class #zdTurns. I only want one
server roundtrip.
The code below fails because renderEmptyTurnOn is expecting a canvas,
but I am passing it a script instead.
Is there an idiomatic way to do this using vanilla seaside and
jquery?
renderMenuOn: h
h div
class: 'zdDialogMenu';
with: [
h span
onClick:
(h jQuery ajax
callback: [ self dialog addTurn ];
script: [ :s | s << ((s jQuery class: #zdTurns) append: (s jQuery html: (self renderEmptyTurnOn: s))) ]);
with: 'Add Turn' ]

You almost nailed it. The argument of an append: message on a jQuery instance accepts a renderable object. This could be a string, a Seaside component or a block. That means you can do it as follows:
renderMenuOn: h
h div
class: 'zdDialogMenu';
with: [
h span
onClick: (h jQuery ajax
script: [ :s |
self dialog addTurn.
s << ((s jQuery class: 'zdTurns') append: [:r | self renderEmptyTurnOn: r ]) ]);
with: 'Add Turn' ]

Related

Conditional Tests in Cypress using data-cy attributes as selectors

I see some posts about this exact topic, but none of them using data classes like I am as selectors, so it makes this conditional test a bit harder to write.
The idea is that I have a table with pagination on it. My idea is to check if the [data-cy-pagination-next] has or doesn't have the disabled attribute on it, which would mean there's more than one page and therefore the test can continue.
Most posts I see use a syntax like this:
cy.get('my-button')
.then($button => {
if ($button.is(':enabled')) {
cy.wrap($button).click()
}
})
But I don't have the $button like they described. What I would be clicking on is a button, but does that really matter?
It doesn't seem like I can write
cy.get('[data-cy=pagination-next]')
.then('[data-cy=pagination-next]' => {
if ('[data-cy=pagination-next]'.is(':enabled')) {
cy.wrap('[data-cy=pagination-next]').click()
}
})
How can I get this conditional to work?
If there is more than one page, this test works great, but in the cases that there is no second page, I just want the test to end there.
Any tips would be greatly appreciated!
Cheers!
Here is the test currently
it('Data Source has Pagination and test functionality', () => {
cy.get('[data-cy=pagination]').should('exist')
// assert that we are at the first page and the start and back button is disabled
cy.get('[data-cy=pagination-page-list]').contains('Page 1 of')
// If there are multiple pages then do the following tests
// click next button and assert that the current page is page 2
cy.get('[data-cy=pagination-next]').click()
cy.get('[data-cy=pagination-page-list]').contains('Page 2 of')
// click end button and assert that the end and next buttons are disabled
cy.get('[data-cy=pagination-end]').click()
cy.get('[data-cy=pagination-next]').should('be.disabled')
cy.get('[data-cy=pagination-end]').should('be.disabled')
// click start button button and assert that the current page is page 1 and next and start buttons are disabled
cy.get('[data-cy=pagination-start]').click()
cy.get('[data-cy=pagination-page-list]').contains('Page 1 of')
cy.get('[data-cy=pagination-start]').should('be.disabled')
cy.get('[data-cy=pagination-back]').should('be.disabled')
})
You can use the page indicator to split the test logic
it('Data Source has Pagination and test functionality', () => {
cy.get('[data-cy=pagination]').should('exist')
cy.get('[data-cy=pagination-page-list]')
.then($pageList => {
if ($pageList.text() === 'Page 1 of 1')
// single page assertions
cy.get('[data-cy=pagination-next]').should('be.disabled')
cy.get('[data-cy=pagination-end]').should('be.disabled')
cy.get('[data-cy=pagination-start]').should('be.disabled')
cy.get('[data-cy=pagination-back]').should('be.disabled')
} else {
// multi page assertions
cy.get('[data-cy=pagination-next]').click()
cy.get('[data-cy=pagination-page-list]')
.should('contain', 'Page 2 of') // assert on second page
cy.get('[data-cy=pagination-end]').click()
cy.get('[data-cy=pagination-next]').should('be.disabled')
cy.get('[data-cy=pagination-start]').click()
cy.get('[data-cy=pagination-page-list]').contains('Page 1 of')
cy.get('[data-cy=pagination-start]').should('be.disabled')
cy.get('[data-cy=pagination-back]').should('be.disabled')
}
})
Better still, control the test data so that only a single page exists, then run two tests under known conditions and eliminate flaky conditional testing.
I think you're misunderstanding how yielding and callbacks work. The reason there is $button in the .then() is because it is yielded by cy.get(). It could be named anything, so long as it is a valid name (note: a string literal, like you are trying to do, is not valid).
So, $button is just the yielded element from your cy.get('my-button'). Which is why we can then use JQuery functions and Chai assertions on it.
cy.get('[data-cy=pagination-next]')
.then($el => { // naming the yielded object from `cy.get()` to $el
if ($el.is(':enabled')) { // using JQuery function `.is` to check if the element is enabled
cy.wrap($el).click() // Cypress requires the JQuery element to be wrapped before it can click it.
}
})
You can do something like this. You can use an each to loop over all the pagination elements. So in case, you don't have only 2 buttons the loop will check for only 2 buttons and then terminate.
cy.get('[data-cy=pagination-page-list]').should('contain.text', 'Page 1 of')
cy.get('[data-cy=pagination-next]').each(($ele, index) => {
if ($ele.is(':enabled')) {
cy.wrap($ele).click()
cy.wrap($ele).should('contain.text', `Page ${index + 2} of`) //index starts from 0
}
})

web2py A() link handling with multiple targets

I need to update multiple targets when a link is clicked.
This example builds a list of links.
When the link is clicked, the callback needs to populate two different parts of the .html file.
The actual application uses bokeh for plotting.
The user will click on a link, the 'linkDetails1' and 'linkDetails2' will hold the script and div return from calls to bokeh.component()
The user will click on a link, and the script, div returned from bokeh's component() function will populate the 'linkDetails'.
Obviously this naive approach does not work.
How can I make a list of links that when clicked on will populate two separate places in the .html file?
################################
#views/default/test.html:
{{extend 'layout.html'}}
{{=linkDetails1}}
{{=linkDetails2}}
{{=links}}
################################
# controllers/default.py:
def test():
"""
example action using the internationalization operator T and flash
rendered by views/default/index.html or views/generic.html
if you need a simple wiki simply replace the two lines below with:
return auth.wiki()
"""
d = dict()
links = []
for ii in range(5):
link = A("click on link %d"%ii, callback=URL('linkHandler/%d'%ii), )
links.append(["Item %d"%ii, link])
table = TABLE()
table.append([TR(*rows) for rows in links])
d["links"] = table
d["linkDetails1"] = "linkDetails1"
d["linkDetails2"] = "linkDetails2"
return d
def linkHandler():
import os
d = dict()
# request.url will be linked/N
ii = int(os.path.split(request.url)[1])
# want to put some information into linkDetails, some into linkDiv
# this does not work:
d = dict()
d["linkDetails1"] = "linkHandler %d"%ii
d["linkDetails2"] = "linkHandler %d"%ii
return d
I must admit that I'm not 100% clear on what you're trying to do here, but if you need to update e.g. 2 div elements in your page in response to a single click, there are a couple of ways to accomplish that.
The easiest, and arguably most web2py-ish way is to contain your targets in an outer div that's a target for the update.
Another alternative, which is very powerful is to use something like Taconite [1], which you can use to update multiple parts of the DOM in a single response.
[1] http://www.malsup.com/jquery/taconite/
In this case, it doesn't look like you need the Ajax call to return content to two separate parts of the DOM. Instead, both elements returned (the script and the div elements) can simply be placed inside a single parent div.
# views/default/test.html:
{{extend 'layout.html'}}
<div id="link_details">
{{=linkDetails1}}
{{=linkDetails2}}
</div>
{{=links}}
# controllers/default.py
def test():
...
for ii in range(5):
link = A("click on link %d" % ii,
callback=URL('default', 'linkHandler', args=ii),
target="link_details")
...
If you provide a "target" argument to A(), the result of the Ajax call will go into the DOM element with that ID.
def linkHandler():
...
content = CAT(SCRIPT(...), DIV(...))
return content
In linkHandler, instead of returning a dictionary (which requires a view in order to generate HTML), you can simply return a web2py HTML helper, which will automatically be serialized to HTML and then inserted into the target div. The CAT() helper simply concatenates other elements (in this case, your script and associated div).

Coding clarity of closure for load event

Is there are clear way to write this closure for the load event on line #4:
for i,item of m
# add item once image loaded
new_item = $("<img src='#{util.image_url(item, 'large')}' />")
new_item.on 'load', ((item) => (=> #add_item(item)))(item)
$("#preload-area").append(new_item)
You want a do loop:
When using a JavaScript loop to generate functions, it's common to insert a closure wrapper in order to ensure that loop variables are closed over, and all the generated functions don't just share the final values. CoffeeScript provides the do keyword, which immediately invokes a passed function, forwarding any arguments.
Something like this:
for i, item of m
do (item) =>
new_item = $("<img src='#{util.image_url(item, 'large')}' />")
new_item.on 'load', => #add_item(item)
$("#preload-area").append(new_item)

Get all attributes or xPath of element in Webdriver

I'm trying to do simple monkey test for my web page, which get all active elements on page and click on them in random order.
When i do this I want to write a log to know, on which element my test click and on which test crashed
So I want log file to look like this
01.01.11 11.01.01 Clicked on Element <span id='myspan' class ='myclass .....>
01.01.11 11.01.01 Clicked on Element <span id='button' class ='myclass title = 'Button'.....>
or
01.01.11 11.01.01 Clicked on Element //*[#id='myspan']
01.01.11 11.01.01 Clicked on Element //*[#id='button']
Is it any way to do in Webdriver + Ruby?
I don't think there is a way but you could always do something like this (with watir-webdriver):
browser.divs.each do |div|
puts '<span ' + ['id','class','title'].map{|x| "#{x}='#{div.attribute_value(x)}'"}.join(' ') + '>'
end
WebDriver does not provide this type of functionality, you would have to get the page source and do some of your own parsing - I've done this in Html Agility Pack with C#, you would need to find a similar library for ruby (see: Options for HTML scraping?)
You can do this:
Get all elements, which are clickable
For example, find all links, find all clickable spans. Put those candidates in a list
Randomly pick a element in that candidate list
Click the very element and write some log
I tweaked #pguardiario's answer to come up with this method:
def get_element_dom_info(#e)
if #e.class != Selenium::WebDriver::Element
raise "No valid element passed: #{#e.class}"
end
#attrs = ['id', 'class', 'title', 'href', 'src', 'type', 'name']
return "<" + #e.tag_name + #attrs.map{ |x| " #{x}='#{#e.attribute(x)}'" if #e.attribute(x) && #e.attribute(x) != "" }.join('') + ">"
end
Of course, it expects that the single parameter you pass into it is an actual Selenium element. Also, it doesn't include every possible attribute, but that's the majority of them (and you can always add your extra attributes if needed).
I suppose you can integrate this via some code like this:
def clickElement(*args)
... # parse vars
#e = #driver.find_element(...)
puts get_timestamp + " Clicked on Element: " + get_element_dom_info(#e)
end
UPDATE
I recently realized that I could get the full html of the element using native javascript (d'oh!). You have to use a hack to get the "outerHTML". Here is my new method:
def get_element_dom_info(how, what)
e = #driver.find_element(how, what)
# Use native javascript to return element dom info by creating a wrapper
# that the element node is cloned to and we check the innerHTML of the parent wrapper.
return #driver.execute_script("var f = document.createElement('div').appendChild(arguments[0].cloneNode(true)); return f.parentNode.innerHTML", e)
end

Seaside: list losing its content on update

This one is really simple. I've got a <select> and I want to update some internal state based on the selection. No need to redisplay anything. The problem is that after the selection and the AJAX request have been made, the list loses its contents.
renderContentOn: html
|value myId|
html form with: [
html select list: #('one' 'two' 'tree' 'four' 'five');
id: (myId := html nextId);
callback: [ :v | value := myId ];
onChange: (
html prototype updater
triggerFormElement: myId;
callback: [:h | value "do something with the value here"];
return: false
).
]
The #updater needs an DOM ID of the element to update. If you fail to provide an ID, it default to this, the DOM element triggering the event. Thus, you end up with an empty list. If you do not need to update something you should use #request instead of #updater. If you want to update something you need to provide a valid ID using #id:.
Read the section AJAX: Talking back to the Server of the Seaside Book, it explains AJAX in great detail.
Because the updater replaces the html of the element it was defined on with the content generated by the callback and your callback does not generate any html, the list is empty, I think.

Resources