Mostrando postagens com marcador javascript. Mostrar todas as postagens
Mostrando postagens com marcador javascript. Mostrar todas as postagens

terça-feira, 16 de julho de 2013

SlimIt, Head.js e django compressor

Ao configurar o django compressor com o filtro do SlimIt, o arquivo do head.js comprimido pela templatetag compress era gerado com um caracter  ï ao ínicio. Utilizando o SlimIt na mão eu obtinha o seguinte erro:

$ slimit head.js
Illegal character '\xbb' at 1:1 after LexToken(ID,'\xef',1,0)
Illegal character '\xbf' at 1:2 after LexToken(ID,'\xef',1,0)

Criei uma issue no repositório do SlimIt acreditando ser um problema desta lib. E determinado a corrigir tal problema acabei descobrindo que a raíz deste era o Head.js. No arquivo fonte desta biblioteca javascript havia realmente um caracter estranho, que nenhum editor exibia, nem mesmo o Vi. No entanto, se abríssemos o arquivo via python com um simples:

open("head.js").read()

Víamos claramente o caracter escondido. Espantosamente o único editor que testei e que exibiu o caracter estranho foi o editor online do github.

Forked, pull resqueted e merged: Felizes para sempre.

quarta-feira, 25 de novembro de 2009

Utilizando asserts para testar layouts

Um dos problemas encontrados em nossos testes de aceitação é a impossibilidade de validar a aparência exata do resultado final de uma determinada ação do usuário. Conseguimos validar com o watir se determinada div possui um texto, se determinado link está presente, mas nada impede que eles estejam escondidos, por trás de outro div, ou com letras da mesma cor do fundo. É sempre útil, mas não necessáriamente exato, sendo sempre um ponto de falha. Ainda mais em um sistema como o que estamos trabalhando em que o visual para o usuário tem uma grande importância.

Eu e o quixadá, mestre do javascript e meu par de hoje, trabalhamos em uma correção de bug visual, e como costumamos trabalhar com desenvolvimento outside-in, chegamos ao dilema de como criar um teste para garantir que a falha existia. A solução encontrada foi inserir asserts dentro do código javascript, tal qual a funcionalidade de asserts do java. O código da função assert ficou parecido com o mostrado abaixo:

function assert(mensagem,valorDesejado,valorRecebido) {
if( valorDesejado != valorRecebido ) {
var html = '<div class="warning">' + mensagem + '</div>';
$('body').append(html);
throw mensagem;
}
}

Em nosso caso, tinhamos que ter a certeza de que após um clique do usuário a barra de rolagem do elemento mantinha-se da mesma forma que anteriormente. Então o código ficou parecido com o mostrado abaixo:

$.get( url, function ( responseHtml ) {
var scrollAnterior = $('#opcoes').scrollTop();
substituiOpcoes( responseHtml );
assert( 'Deveria manter scroll igual', scrollAnterior, $('#opcoes').scrollTop() );
});

No teste de aceitação verificamos então que o texto 'Deveria manter scroll igual' não deveria aparecer. O código do step do cucumber utilizando o watir pode ser visto abaixo:

Then /^a barra de rolagem deveria permanecer na mesma posição$/ do
@browser.text.should_not include('Deveria manter scroll igual')
end

Com o teste pronto e falhando, aí sim corrigimos a função javascript substituiOpcoes(html) de forma a manter o scroll anterior.

Não é uma solução perfeita. Estamos pesquisando uma melhor, como pode ser vista no post Testes de aceitação automáticos para Flash com T-Plan Robot do Anselmo. Mas enquanto isso podemos evitar alguns pontos de falha visuais que costumávamos ter que testar manualmente. Dado que nosso sistema possui 100% de cobertura de testes unitários, somados a 152 cenários de teste de aceitação que abragem 1568 passos, acho que estamos indo por um bom caminho.

sábado, 1 de agosto de 2009

TralhaController notificando observadores por pattern

TralhaController é uma biblioteca javascript que permite que aplicações web que utilizam requisições assíncronas ofereçam URLs para cada contexto de navegação do usuário. Para saber mais sobre ela basta ler o artigo Mantendo contexto usando ajax que o Bruno Carvalho escreveu recentemente.

Porém uma coisa me incomodava no uso dessa classe. Para cada observador registrado eu precisava dentro do método update verificar se ele precisava agir de acordo com a URL. Isso acaba por tornar o código mais confuso pois obrigava-nos a misturar validações e lógica de negócio. Veja um exemplo abaixo:

var observadorBusca = { update:function(url) {
  if( url.indexOf("#busca=")!=-1 ) {
    //Executa a busca
  }
} };
var observadorLink = { update:function(url) {
  if( url.indexOf("#link=")!=-1 ) {
    //Executa o link clicado
  }
} };
TralhaController.addObserver(observadorBusca);
TralhaController.addObserver(observadorLink);

Para tentar melhorar isso, pus a mão na massa e alterei o TralhaController permitindo registrar observadores que só fossem notificados diante de URLs que tivessem o padrão desejado. Utilizando então a última versão da biblioteca o código acima poderia então ser alterado para ficar da seguinte forma:

var observadorBusca = { update:function(url) { /* Executa a busca */ } };
var observadorLink = { update:function(url) { /* Executa o link clicado */ } };

TralhaController.addObserver("\\#busca\=.*", observadorBusca);
TralhaController.addObserver("\\#link\=.*", observadorLink);

Bem mais simples certo? É claro que há aqueles que não são muito fãs de expressões regulares e vão achar a primeira forma melhor. Para esses não há problema pois a forma antiga continua funcionando. Observadores registrados sem padrões continuam sendo notificados a toda e qualquer modificação de URL.

quinta-feira, 2 de outubro de 2008

Impedindo redeclaração de objetos javascript

Ao importar duas vezes um mesmo arquivo .js em uma página, as funções, protótipos e objetos são redeclarados. Se for necessário que um determinado objeto mantenha seu estado durante as iterações assíncronas dessa página, perde-se este estado na segunta importação do arquivo .js.

Veja por exemplo o objeto abaixo, que nada mais é que um repositório de outros objetos. Tá ta ta, não é um exemplo que tenha tanta utilidade, mas vá lá, é só pra explicar o mecanismo.
var Repositorio = {
sequencia:0,
lista:[],
registrar:function(obj) {
var id = ++this.sequencia;
this.lista[ id ] = obj;
return id;
},
recuperar:function(id) {
return this.lista[ id ];
}
}
Agora estando esse código num arquivo por exemplo repositorio.js teríamos o problema relatado com o código a seguir:
<script src="repositorio.js"></script>
<script>
Repositorio.registrar( new Object() );
Repositorio.registrar( new Object() );
</script>
<script src="repositorio.js"></script>
<script>
alert( Repositorio.recuperar( 1 ) );
</script>
Para impedir essa redeclaração do objeto, e os possíveis erros que ela pode causar, basta tentar utilizá-lo dentro de um bloco try e fazer sua declaração dentro do bloco catch. Ou seja, ele só será declarado caso haja um erro na tentativa de usá-lo. Veja:
try{ Repositorio.existo(); }
catch(e) {
Repositorio = {
sequencia:0,
lista:[],
existo:function(){},
registrar:function(obj) {
var id = ++this.sequencia;
this.lista[ id ] = obj;
return id;
},
recuperar:function(id) {
return this.lista[ id ];
}
}
}
Mas aí você poderia me dizer: Pô pra que tudo isso, basta não importar duas vezes o javascript. Acontece que se você está criando uma biblioteca para ser distribuída para diversos sites e blogs, você não deve subestimar a "criatividade" dos utilizadores dela. Então, o melhor é se precaver.

terça-feira, 16 de setembro de 2008

On-demand javascript com PHP

A técnica de script on-demand serve para compartilhar dados entre domínios. A idéia dela é driblar as restrições da classe XMLHttpRequest, que só permite requisições no mesmo domínio, e a restrição do iframe que no máximo permite comunicação entre sub-domínios de um mesmo domínio.

Basicamente consiste em adicionar um script à página quando uma informação é necessária. Esse script proverá a informação necessária chamando uma função de callback ao solicitador. Uma explicação mais detalhada sobre a técnica pode ser encontrada em On-Demand_Javascript - AjaxPatterns.

Com PHP fica ainda mais fácil desenvolver o script provedor de dados. Digamos por exemplo que você queira obter a lista de livros de uma biblioteca, seu javascript ficaria assim:
var Livros = {
listar:function() {
var s = document.createElement('script');
s.type = 'text/javascript';
s.src = 'listaDeLivros.php?callback=Livros.onload_listar';
document.getElementsByTagName('head')[0].appendChild(s);
}
onload_listar:function(html) {
document.getElementsByTagName('lista').innerHTML = html;
}
}

A função listar insere um javascript à página informando a ele a função javascript de callback. No PHP informaremos o html gerado chamando a função enviada no parâmetro callback. Mais ou menos como mostrado abaixo:
<?php echo $_REQUEST['callback']; ?>("<h1>Lista de livros</h1>");

Mas é claro que normalmente o html gerado não será de apenas uma linha. Além disso ele poderá ter diversos dados com aspas e outros caracteres que poderiam causar erros de javascript. Ficar se preocupando com esse tipo de coisa na diagramação do html é trabalhoso e passível de muitos erros.

Para evitar isso podemos utilizar a função ob_start, que faz com que tudo o que for escrito no output seja guardado em buffer e ao final remetido a uma função de callback do PHP. Nesta função então pode-se tratar o html e enfim escrever no real output para o cliente. Veja como ficaria:
<?php 
function captura_buffer($html) {
$html = addslashes($html);
$html = str_replace("\n", '\n', $html);
$html = str_replace("\r", '\r', $html);
return $_REQUEST['callback'] . '("' . $html . '");';
}
ob_start('captura_buffer');

$livros = funcao_ficticia_para_obter_livros();

foreach($livros as $livro) {
?>

<p>
<?php echo $livro->id ?> -
<b><?php echo $livro->titulo ?></b> -
<?php echo $livro->autor ?>
</p>

<?php } ?>

sexta-feira, 4 de abril de 2008

Brincando de extensões com o chrome do Firefox

Desenvolver extensões para o Firefox é algo muito divertido. Mas há certas questões que nos deixam de cabelo em pé. Basicamente por falta de exemplos e tutoriais simples. No caso, eu procurava duas coisas: Como acessar o elemento html que foi clicado com o botão direito do mouse, e como copiar um determinado trecho selecionado para a área de transferências (clipboard).

Nessas horas então, nada melhor que analisar os códigos contidos no Chrome do Firefox. Descompactando os arquivos browser.jar e toolkit.jar, localizados no diretório ${FIREFOX_HOME}/chrome, podemos verificar diversos bons exemplos.

No arquivo browser.js do pacote browser.jar por exemplo, pude verificar que para acessar o elemento clicado com o botão direito basta utilizar a notação document.popupNode. O exemplo abaixo mostra de maneira simples um Xul e um Js de um item no menu do botão direito cuja única função é exibir a tag html do elemento clicado com o botão direito.

Xul:
<popup id="contentAreaContextMenu">
<menuitem id="itemMenuExibirTag" label="Exibir tag" oncommand="exibirTag()"/>
</popup>
Js:
function exibirTag() {
alert( document.popupNode.tagName );
}
Já para implementar a função de copiar para a área de transferência, verifiquei no arquivo browser.xul do pacote browser.jar que os menus de copiar do próprio navegador chamavam a função goDoCommand com o parâmetro cmd_copy. Encontrei essa função no arquivo globalOverlay.js do pacote toolkit.jar. Havia nele um comentário informando que a função era apenas para manter compatibilidade com versões antigas, e realmente a única coisa que ela fazia era repassar a função para o método doCommand da classe CommandUpdater. Abaixo mostro um exemplo de item que seleciona todos os itens de uma página e copia o conteúdo para o clipboard:

Xul:
<popup id="contentAreaContextMenu">
<menuitem id="itemMenuCopiarPagina" label="Copiar pagina" oncommand="copiarPagina()"/>
</popup>
Js:
function copiarPagina() {
CommandUpdater.doCommand('cmd_selectAll');
CommandUpdater.doCommand('cmd_copy');
}

quarta-feira, 19 de março de 2008

GreaseMonkey para feed do InfoBlogs no GoogleReader

Quem assina o feed do InfoBlogs deve ter o mesmo problema que eu. O link deste feed não é para o post em si, ele é para uma cópia do post na página do próprio InfoBlogs. Particularmente acho isso irritante, pois só costumo entrar no blog do autor quando o post é interessante e desejo comentá-lo. Nesse caso, preciso aguardar o carregamento (que é bem lento) da página do InfoBlogs para só depois poder clicar e ver o post original, onde eu posso comentar.

Para resolver esse problema implementei um script no GreaseMonkey para remontar a página do GoogleReader com os links corretos. Para quem não sabe o GreaseMonkey é uma extensão do Firefox, o melhor navegador existente na atualidade, que permite ao usuário configurar scripts para serem rodados ao término do carregamento de uma determinada página. Eu falei brevemente dele no post em que falo sobre as extensões do Firefox essenciais para um desenvolvedor web, e agora com este exemplo ficará mais fácil entender como ele funciona.

O código do script encontra-se abaixo. O que ele faz é verificar a cada 3 segundos se houve alguma alteração de estado no GoogleReader. Para então mudar os links caso haja necessidade. Só há uma ressalva, ele não funciona para o modo de visualização de lista, só a visão expandida. O motivo é que não gosto da visualização em lista e portanto não me interessei em testar nela :P
// ==UserScript==
// @name GoogleReaderGM
// @namespace http://www.google.com/reader
// @include http://www.google.com/reader/*
// ==/UserScript==

function GoogleReaderGM() {}
GoogleReaderGM.prototype = {
isChanged: function() {
var e = document.getElementById("entries");
var ds = e.getElementsByTagName("div");
if( this._oldUrl != window.location.href ) {
this._oldUrl = window.location.href;
this._oldQtds = ds.length;
return true;
}
if( this._oldQtds != ds.length ) {
this._oldQtds = ds.length;
return true;
}
return false;
},
mudaLinksInfoBlogs: function() {
if( this.isChanged() ) {
var e = document.getElementById("entries");
var as = e.getElementsByTagName("a");
for( i=0; i<as.length; i++ ) {
var a = as[i];
if( a.className=="entry-title-link" && a.href.indexOf("www.infoblogs.com.br/view.action")!=-1 ) {
a.href = a.href.replace("view","redir/go");
}
}
}
}
}

var gr = new GoogleReaderGM();

setInterval(function() { gr.mudaLinksInfoBlogs(); }, 3000);
Como pode ser visto eu defino como um comentário JavaScript em quais páginas esse script será executado. No caso, todas aquelas abaixo de http://www.google.com/reader.

O interessante é que se você quiser pode utilizar esse código como template para executar outras funcionalidades no GoogleReader. Basta adicionar mais alguma função, e utilizando isChanged você fica sabendo se alguma entrada nova apareceu na área que exibe as entradas dos feeds. Isso é claro se você utilizar a visão expandida (Expanded view), pois o método isChanged só foi feito pra ela.

quinta-feira, 13 de março de 2008

Escapando textos em funções de javascript executadas em links

Para determinar a execução de uma função javascript no clique de um link temos duas possibilidades. Ou utilizamos o próprio atributo href da tag a com o protocolo javascript: ou adicionamos o atributo onclick fazendo a chamada à função.

Contudo o comportamento de ambos não é totalmente igual. Principalmente quando chamamos funções que passam como argumento algum texto escapado com a função escape. Normalmente usa-se esse escape para evitar error de javascript devido ao uso de aspas simples e duplas nos textos.

Veja abaixo dois exemplos de cógigo com o erro por não usar escape. O primeiro com aspas simples, que faz com que o javascript ache que o texto terminou antes do que deveria e o segundo com aspas duplas fazendo o navegador acreditar que o atributo terminou:
var textoComAspaSimples = "Quero um copo d'água";
var s = '<a href="javascript:alert(\'' + textoComAspaSimples+ '\')">Clique aqui</a>';
document.writeln(s);

var textoComAspaDupla = 'Você está com "sede"?';
var s = '<a href="javascript:void(0)" onclick="alert(\'' + textoComAspaDupla+ '\')">Clique aqui</a>';
document.writeln(s);
Para evitar esses erros uma boa idéia é utilizar a função escape. No entanto isso não funciona direito por href. Neste atributo o navegador interpreta o código escapado causando o mesmo erro. Isso pode ser observado no exemplo a seguir, no primeiro link gerado o erro acontece, no segundo não:
var textoComAspaSimples = "Quero um copo d'água";
var s = '<a href="javascript:alert(\'' + escape(textoComAspaSimples) + '\')">Clique aqui href</a><br/>';
s += '<a href="javascript:void(0)" onclick="alert(\'' + escape(textoComAspaSimples) + '\')">Clique aqui onclick</a>';
document.writeln(s);
O problema é que com essa solução a função que recebe este parâmetro precisa executar uma transformação reversa utilizando a função unescape. Caso contrário veremos algo parecido com o que o segundo link do exemplo acima mostra: Quero%20um%20copo%20d%27%C3%A1gua.

Implementando a transformação reversa na função que recebe teríamos algo como o mostrado abaixo. Uma nova função que recebe o parâmetro escapado e o transforma para utilizá-lo:
function escapedAlert(s) {
alert( unescape(s) );
}
var textoTodoErrado = "Sentes \"sede\"?\nQuero um copo d'água";
var s = '<a href="javascript:void(0)" onclick="escapedAlert(\'' + escape(textoTodoErrado) + '\')">Clique aqui onclick</a>';
document.writeln(s);
Mas isso ainda não é o ideal pois continuaríamos sem poder utilizar a função dentro do atributo href, e além disso precisaríamos sempre preparar as funções para receber parâmetros escapados. Uma outra alternativa é criar nossa própria função de escape. Como é mostrado no exemplo abaixo:
function escapeForLink(s) {
if(s) {
var r = s.split("\n").join("\\n");
r = r.split("\r").join("\\r");
r = r.split("'").join("\\u0027");
r = r.split('"').join("\\u0022");
return r;
}
return s;
}
var textoTodoErrado = "Sentes \"sede\"?\nQuero um copo d'água";
var s = '<a href="javascript:alert(\'' + escapeForLink(textoTodoErrado) + '\')">Clique aqui href</a><br/>';
s += '<a href="javascript:void(0)" onclick="alert(\'' + escapeForLink(textoTodoErrado) + '\')">Clique aqui onclick</a>';
document.writeln(s);
Com essa função os problemas de escape para passagem de parâmetro de texto em funções executadas apartir de links fica resolvido.

terça-feira, 18 de dezembro de 2007

Como obter a largura e a altura de Divs liquidos

Obter os atributos width e height de Divs absolutos é muito simples. A coisa começa a ficar complicada quando seu Div não possui uma altura e uma largura fixa, se comportando de acordo com o restante da página. Encontrei em alguns blogs diversas dicas de como fazer isso. Alguns só funcionavam no Firefox, outros só no Internet Explorer, nenhum em ambos.

Com essas informações então criei as funções getWidth e getHeight que retornam a largura e altura de um div, mesmo que está seja fluída:

function getHeight(d) {
if( document.all ) {
return eval( "document.all." + d.id + ".offsetHeight" );
} else {
return parseInt( getStyle(d,"height") );
}
}

function getWidth(d) {
if( document.all ) {
return eval( "document.all." + d.id + ".offsetWidth" );
} else {
return parseInt( getStyle(d,"width") );
}
}

function getStyle(el, style) {
if(!document.getElementById) return;
if( window.getComputedStyle ) {
value = document.defaultView.getComputedStyle(el, "").getPropertyValue(style);
} else if(document.defaultView) {
value = document.defaultView.getComputedStyle(el, "").getPropertyValue(style);
} else if(el.currentStyle) {
value = el.currentStyle[style];
}
return value;
}


Para usá-las é só chamar enviando por parâmetro um objeto div. Veja o exemplo:

var d = document.getElementById("meuDiv");
alert( getWidth( d ) );
alert( getHeight( d ) );


Testei essas funções com o Firefox 2, IE 7 e o Safari 3.

sexta-feira, 17 de agosto de 2007

O atributo length de objetos e arrays em javascript

Eu não resisti e usei Javascript essa ultima semana inteira - É a minha confissão de hoje na reunião de javascriptólatras anônimos - Passei um perrengue danado por causa de suas peculiaridades. Tudo causado por falta de atenção a uma das pequenas regrinhas que essa linguagem viciante tem. Para evitar que outros passem pelo mesmo problema, compartilho aqui minha dor.

Em primeiro lugar é preciso verificar que somente arrays têm o atributo length. Em segundo que arrays podem se comportar como objetos, e objetos podem se comportar como arrays, e que arrays são objetos. Entendeu? Bom, vamos aos exemplos pra ficar mais claro.
 var eu = { nome:"Tiago",idade:25 }; //Criando um objeto dinâmico com {}
alert( eu["nome"] ) //Acessando como se fosse um mapa
alert( eu.idade ) //Acessando como um atributo
eu.sexo = "Sempre"; //Criando um novo atributo
alert( eu["sexo"] );
eu["altura"] = 1.75; //Criando um novo atributo como mapa
alert( eu.altura );
No exemplo acima eu crio um objeto dinamicamente utilizando chaves além de acessar e criar novos atributos, de duas formas possíveis: Como se fosse um mapa, utilizando o operado colchetes, e da maneira mais comum, que é utilizando o operador ponto. Vamos seguir então para o próximo exemplo:
 var eu = []; //Criando um array com []
eu.nome = "Tiago"; //Setando atributo em um array? Que coisa estranha
eu["idade"] = 25 //Não era um array, por que está aceitando uma chave como se fosse um mapa?
alert( eu["nome"] ) //Acessando como se fosse um mapa
alert( eu.idade ) //Acessando como um atributo
eu.sexo = "Sempre"; //Criando um novo atributo
alert( eu["sexo"] );
eu["altura"] = 1.75; //Criando um novo atributo como mapa
alert( eu.altura );
Neste exemplo criamos um array ao invés de um objeto, e qual não é nossa surpresa ao perceber que, exceto pela incialização, o comportamento dele fica exatamente igual ao de um objeto. Isso ocorre porque um array é um objeto. Até aí tudo parece maravilhoso. A coisa desanta quando você resolve usar o comportamento de array, num array tratado como objeto. Vejamos:
 var eu = []; 
alert(eu.length); //Tamanho 0 oras, não coloquei nada aqui ainda
eu["nome"] = "Tiago"; //Inserindo um atributo
alert(eu.length); //Tamanho 0?? E o atributo que botei?
eu.idade = 25; //Inserindo outro atributo
alert(eu.length); //Tamanho 0 de novo????
eu[0] = "primeiro indice"; //Adicionando elementos ao array
eu[1] = "segundo indice";
alert(eu.length); //Ahhh agora sim
eu.push("terceiro indice");
eu.push("quarto indice");
alert(eu.length); //Ahhh agora sim de novo
No exemplo acima podemos perceber o quão confuso começa a ficar. Basicamente o atributo length passa a só funcionar quando inserimos elementos no array. Ou seja, os atributos inseridos no array ficam fora do array. Isso fica mais evidente fazendo as iterações como as do exemplo abaixo:
 //agora vamos iterar só pelos elementos do array
for(i=0; i {
alert("elemento do array: " + eu[i]);
}
//agora vamos iterar sobre tudo que tiver aí
for(i in eu)
{
alert("atributo do objeto: " + eu[i]);
}
//De novo somente elementos do array
while(eu.length>0)
{
alert("Remove elemento: " + eu.pop());
}
Na primeira eu itero sobre números, logo somente sobre os elementos do array. No segundo eu itero sobre as chaves, sejam elas numéricas, sejam de texto. E finalmente no terceiro eu itero removendo os elementos do array. Mas ainda sim podem ocorrer algumas curiosidades que podem nos confundir, veja abaixo:
 var eu = []; 
eu[0] = "primeiro indice";
eu[1] = "segundo indice";
eu["1"] = "substitui segundo indice"; //Isso aqui é o mesmo que eu[1]
alert(eu[1]); //Vai alertar a mesma coisa que o alerta de baixo
alert(eu["1"]);
eu["length"] = 0; //Vou sacanear o array agora
alert(eu.length); //Zerei o tamanho
alert(eu[4]); //Ih!!! Apagou tudo
Perceba que adicionar um elemento ao indice 1 como número é o mesmo que adicionar ao 1 como string. Além disso, veja que podemos alterar o valor do tamanho, atributo length, do array somente atribuindo um valor a ele. Isso pode causar grandes problemas, ainda mais se você estiver guardando dados digitados pelo usuário como se o array fosse um mapa.

Um exemplo simples é um usuário preenchendo um formulário com campo de nome e valor, ao clicar em um botão esses dados são inseridos no array como se fosse um mapa. Digamos que ele preencha da seguinte maneira:
Nome     | Valor: 
---------------------
"país" | "brasil"
10 | "isso"
7 | "hoje"
"olha" | "caso"
"length" | 0
Neste caso quando ele digitar o nome 10, o array passará a ter 10 posições, mas no momento que ele digitar "length" com o valor 0, os dados que ele inseriu com nome 10 e 7 desaparecerão. Para debugar isso você terá que arrancar vários tufos de seu cabelo.

E é aí finalmente que chegamos ao ponto crucial. Para evitar esse tipo de problema o ideal é deixar bem separado o que é array, inicializando com [] e o que é objeto inicializando com {}. Isso porque para objetos, um atributo com nome length não tem poder algum, exceto se você implementá-lo para que tenha.

quarta-feira, 16 de maio de 2007

Teste unitário em funções javascript utilizando o JUnit

Um dos grandes problemas do javascript é a falta de testabilidade. Vou mostrar aqui nesse artigo uma forma que encontrei de testar as funções de arquivos javascripts utilizando o JUnit. Ah! Claro, isso só é possível se você estiver utilizando o Java 6, que possui suporte a linguagens de script e já vem nativamente com suporte a javascript.

Primeiro criamos o arquivo funcoes.js contendo uma função para validar um determinado formulário. O objetivo é retornar verdadeiro se todos os campos estiverem preenchidos e falso no caso contrário. O formulário html deve ter dois campos: nome e senha. Inicialmente deixaremos essa função sem nenhum código, pois criaremos os testes primeiro:
function validaFormulario(f)
{
return false;
}

Em seguida criamos uma classe para realizar os testes unitários contendo o método testValidaFormulario e outras duas classes internas para servir de Mock do formulário html e dos campos do formulário, HtmlFormMock e HtmlFieldMock respectivamente. Segue o código:
package junit.script;
import junit.framework.TestCase;

public class FuncoesTest extends TestCase
{
public void testValidaFormulario() throws FileNotFoundException, ScriptException, NoSuchMethodException
{
//Aqui entrarão os testes
}

public class HtmlFormMock implements Serializable
{
public HtmlFieldMock nome = new HtmlFieldMock();
public HtmlFieldMock senha = new HtmlFieldMock();
}
public class HtmlFieldMock implements Serializable
{
public String name;
public String value;
}
}

No método testValidaFormulario da classe de testes começamos então importanto o script funcoes.js, e criando um Invocable para poder executar funções no javascript:
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("js");
engine.eval( new FileReader( "C:/Projetos/testes/web/funcoes.js" ) );
Invocable invocable = (Invocable) engine;

Em seguida preenche-se o Mock de formulário da seguinte forma:
HtmlFormMock f1 = new HtmlFormMock();
f1.nome.name = "nome";
f1.nome.value = "Tiago";
f1.senha.name = "senha";
f1.senha.value = "123456";

Tendo o invocador de funções de script (Invocable) e o objeto mock, basta alterar os atributos deste objetos para utilizá-lo como parâmetro do formulário por diversas vezes. Veja abaixo os testes que defini para esta função, repare que fiz dois testes para verificar se o campo "nome" e o campo "senha" existem no formulário, pois é possível que criem um formulário html sem o campos:

//Todo formulario preenchido
Boolean b = (Boolean) invocable.invokeFunction("validaFormulario", new Object[] { f1 } );
assertTrue("Formulario todo preenchido deve retornar true", b.booleanValue());

//Senha preenchida com nulo
f1.senha.value = null;
b = (Boolean) invocable.invokeFunction("validaFormulario", new Object[] { f1 } );
assertFalse("Senha preenchida com nulo deve retornar falso", b.booleanValue());

//Senha preenchida com vazio
f1.senha.value = "";
b = (Boolean) invocable.invokeFunction("validaFormulario", new Object[] { f1 } );
assertFalse("Senha preenchida com vazio deve retornar falso", b.booleanValue());

//Nome preenchido com nulo
f1.senha.value = "outra";
f1.nome.value = null;
b = (Boolean) invocable.invokeFunction("validaFormulario", new Object[] { f1 } );
assertFalse("Nome preenchido com nulo deve retornar falso", b.booleanValue());

//Nome preenchido com vazio
f1.nome.value = "";
b = (Boolean) invocable.invokeFunction("validaFormulario", new Object[] { f1 } );
assertFalse("Nome preenchido com vazio deve retornar falso", b.booleanValue());

//Campo nome do formulario nao existir deve retornar falso
f1.nome = null;
b = (Boolean) invocable.invokeFunction("validaFormulario", new Object[] { f1 } );
assertFalse("Campo nome do formulario nao existe entao deve retornar falso", b.booleanValue());

//Campo senha nao pode ser nulo
f1.nome = new HtmlFieldMock();
f1.nome.name = "nome";
f1.nome.value = "Tiago";
f1.senha = null;
b = (Boolean) invocable.invokeFunction("validaFormulario", new Object[] { f1 } );
assertFalse("Campo senha do formulario nao existe entao deve retornar falso", b.booleanValue());

Executando esse teste com o JUnit teremos uma falha logo no primeiro teste, pois o este espera que haja um retorno de true e nossa função javascript está sempre retornando false. Alteremos então a função javascript dando-lhe um código aceitável:
function validaFormulario(f)
{
if( f.nome && f.nome.value && f.nome.value.length() > 0 &&
f.senha && f.senha.value && f.senha.value.length() > 0 )
{
return true;
}
return false;
}

Pronto, tendo a função javascript codificada, basta rodarmos o teste no JUnit novamente para verificar que finalmente a função está funcionando de acordo com o específicado. E o melhor, sempre que ocorrer uma alteração no código dela, saberemos se houve alguma quebra de contrato.

Também é possível testar métodos de classes javascript. Basta utilizar o método invokeMethod da interface Invocable, ao invés de invokeFunction. Mas isso eu deixo pra outro artigo.

quinta-feira, 3 de maio de 2007

Problemas com Post do XMLHttpRequest

Recentemente tive problemas ao tentar utilizar chamadas assíncronas no javascript utilizando o método post do protocolo http. Na verdade, o que ocorria é que os parâmetros enviados por post não eram recebidos em meu servlet. Descobri então como contornar isso, mas não entendi o porque desse problema ter ocorrido.

No javascript eu fazia a seguinte chamada, onde a variavel r era um objeto XMLHttpRequest:
r.open("POST", "http://localhost/MeuServlet", true);
r.send("nome=tiago");

No servlet MeuServlet eu apenas pegava o parâmetro nome e o imprimia na saída padrão:
String nome = request.getParameter("nome");
System.out.println("nome enviado = " + nome);

A saída era sempre "nome enviado = null". Olhando no debugger de chamadas assíncronas do firebug dava pra ver que os parâmetros estavam sendo enviados com sucesso. O problemas estava mesmo na recepção. Eis então que achei um exemplo na internet com o que faltava para meu código javascript:
r.open("POST", "http://localhost/MeuServlet", true);
r.setRequestHeader("Content-Type", "application/x-www-form-urlencoded;");
r.send("nome=tiago");

Colocando essa linha indicando o Content-Type no header da requisição a chamada por post passou a funcionar. Se alguém tiver uma explicação do porque disso será de grande ajuda.

quarta-feira, 2 de maio de 2007

Rolar um DIV via javascript

Segue um exemplo de como rolar um div via javascript:
function praBaixo()
{
var c = document.getElementById("meuId");
c.scrollTop = c.scrollHeight;
}

No exemplo, a função "praBaixo" colocaria a barra de rolagem de um div para baixo de forma a mostrar o ultimo conteudo do div. É bom lembrar que para um div ter barra de rolagem é preciso indicar seu tamanho, width para rolagem horizontal e height para rolagem vertical, e a propriedade "overflow: auto" no css.

segunda-feira, 9 de abril de 2007

A busca de seu site no firefox

ATENÇÃO: O seguinte artigo mostra como adicionar uma busca somente no Firefox. Para ver como adicionar em diversos navegadores, como o próprio Firefox e o Internet Explorer, utilizando o padrão OpenSearch abra o artigo Busca com OpenSearch no Firefox e IE7




Quer disponiblizar a busca de seu site ou blog direto no Firefox? Isso é fácil fácil, basta primeiro criar um arquivo src contendo um xml nos moldes do seguinte exemplo:
<search 
name="Titulo do site"
description="Descricao do site"
action="http://endereco.do.site/busca"
searchform="http://endereco.do.site/busca"
method="GET"
version="1.0">
<input name="q" user="">
</search>
<browser
update="http://endereco.do.site/busca-firefox.src"
updateicon="http://endereco.do.site/busca-firefox.png"
updatecheckdays="7">
</browser>


Calma calma, parece um descritor complicado mas não é. Primeiro na tag search devemos preencher o titulo do site e depois a descrição. O atributo action indica o executor da busca: um servlet, cgi, o que for, que receberá um determinado parâmetro de busca. O nome desse parâmetro é indicado na tag input.

A tag browser indica quando esses descritor deve ser atualizado e onde deve ser buscado. Veja que tem o endereço de um icone, atributo updateicon. É importante que o tamanho desse icone seja 16x16, caso contrário fica esquisitão.

Depois de criado seu e disponibilizado o descritor, basta colocar o link para que os usuários adicionem sua busca no firefox. Segue o exemplo do javascript que faz isso:
<a href="javascript:window.sidebar.addSearchEngine('http://endereco.do.site/busca-firefox.src','http://endereco.do.site/busca-firefox.png','Busca no meu site','png');">
Adiciona busca do meu site
</a>


Recentemente disponibilizamos aqui na globo.com a busca do nosso portal de vídeos, o Globo Vídeos, para ser colocado no firefox. Veja como fica:

Adiciona busca do Globo Vídeos

terça-feira, 3 de abril de 2007

ClassID do Windows Media Player embed

Quem está colocando um Windows Media Player embed em uma página precisa ficar atento sobre as configurações que funcionam e as que não funcionam para um determinado classid. Contudo, isso só é necessário para o Internet Explorer. No Firefox, que utiliza a tag embed ao invés da object não há esse problema.

O que descobri é que para o classid 6BF52A52-394A-11d3-B153-00C04F79FAA6, que seria para WMP 7 ou superior, os parâmetros ShowControls e ShowStatusBar não funcionam. Assim, para poder mecher no layout do player deve-se utilizar o parâmetro uiMode, parâmetro que tem sérias limitações que estão me dando uma baita dor de cabeça.

Isso porque o que eu preciso é de um player embed que só possua a barra de status, mas não achei até agora um valor que faça isso para o parâmetro uiMode.

O outro classid do windows media player, 22d6f312-b0f6-11d0-94ab-0080c74c7e95, serve apenas para o WMP com versão inferior a 7. Com este classid é possível utilizar todos os parâmetros que preciso. Mas, como os arquivos que desejo abrir precisam de codecs específicos de versões superioras, não posso utilizar esse classid.

Ou seja, até agora estou ferrado.

Atualização: Solução proposta em Windows Media Player embed só com a barra de status