Casos de prueba
Especificaciones de pruebas funcionales y de API, con requisitos asociados, precondiciones, datos de entrada, pasos de ejecución y resultados esperados.
Consultar casos de prueba →Pruebas funcionales, automatización, regresión y documentación orientadas a verificar el comportamiento del software, detectar defectos y aportar evidencia reproducible.
Biblioteca interactiva de escenarios de prueba documentados para verificar el comportamiento del portfolio.
Pruebas automatizadas de interfaz para verificar validaciones, flujos de acceso y comportamiento funcional del portfolio.
Utilizo Selenium WebDriver para interactuar con la interfaz como lo haría un usuario y Node.js Test Runner para verificar los resultados mediante aserciones automatizadas.
Comprueba la carga directa de las cinco páginas internas y la navegación a ellas mediante los enlaces visibles del portfolio, verificando la URL y el contenido principal.
const paginas = [
'cv.html',
'frontend.html',
'backend.html',
'qa.html',
'quality-management.html'
];
// Prueba 1: carga directa de cada página.
for (const pagina of paginas) {
await driver.get(`${BASE_URL}/${pagina}`);
await driver.wait(until.elementLocated(By.css('body')), 10000);
const url = await driver.getCurrentUrl();
assert.ok(url.endsWith(`/${pagina}`));
const contenido = await driver.findElement(By.css('main'));
assert.equal(await contenido.isDisplayed(), true);
}
// Prueba 2: navegación desde enlaces del portfolio.
for (const pagina of paginas) {
await driver.get(`${BASE_URL}/index.html`);
await driver.executeScript(
"sessionStorage.removeItem('portfolioSession');"
);
await driver.navigate().refresh();
// Se completa el formulario de acceso con datos válidos.
// Se espera hasta que #portfolio esté visible.
const enlace = await driver.wait(
until.elementLocated(By.css(`a[href="${pagina}"]`)),
10000
);
assert.equal(await enlace.isDisplayed(), true);
await enlace.click();
await driver.wait(until.urlContains(`/${pagina}`), 10000);
assert.ok((await driver.getCurrentUrl()).endsWith(`/${pagina}`));
}
Extractos de las dos pruebas reales. La segunda también completa el formulario de acceso y espera que el portfolio sea visible antes de hacer clic. Archivo: portfolio-navigation.test.js.
Verifica que el logo de cada página interna regrese a index.html y que los botones de retorno conduzcan a #about desde CV o a #qa desde las otras cuatro páginas, con la sección de destino visible.
// Prueba 1: retorno mediante el logo.
for (const pagina of paginas) {
await driver.get(`${BASE_URL}/${pagina}`);
const enlace = await driver.wait(
until.elementLocated(By.css('a.logo[href="index.html"]')),
10000
);
assert.equal(await enlace.isDisplayed(), true);
await enlace.click();
await driver.wait(until.urlContains('/index.html'), 10000);
assert.ok((await driver.getCurrentUrl()).endsWith('/index.html'));
}
// Prueba 2: retorno mediante botones a secciones del Home.
const retornos = [
{ pagina: 'cv.html', seccion: 'about' },
{ pagina: 'frontend.html', seccion: 'qa' },
{ pagina: 'backend.html', seccion: 'qa' },
{ pagina: 'qa.html', seccion: 'qa' },
{ pagina: 'quality-management.html', seccion: 'qa' }
];
// Antes del recorrido se inicia una sesión válida.
for (const { pagina, seccion } of retornos) {
await driver.get(`${BASE_URL}/${pagina}`);
const boton = await driver.wait(
until.elementLocated(
By.css(`a.cv-back-button[href="index.html#${seccion}"]`)
),
10000
);
assert.equal(await boton.isDisplayed(), true);
await boton.click();
await driver.wait(
until.urlIs(`${BASE_URL}/index.html#${seccion}`),
10000
);
assert.equal(
await driver.getCurrentUrl(),
`${BASE_URL}/index.html#${seccion}`
);
const destino = await driver.wait(
until.elementLocated(By.id(seccion)),
10000
);
await driver.wait(until.elementIsVisible(destino), 10000);
assert.equal(await destino.isDisplayed(), true);
}
Extractos de las dos pruebas reales. La segunda inicia una sesión válida antes de recorrer las cinco páginas y comprueba la visibilidad de cada sección de destino. Archivo: portfolio-navigation.test.js.
Comprueba que la página se abra, que el título sea correcto y que el formulario de acceso esté visible.
const title = await driver.getTitle();
assert.equal(
title,
'Miguel Flor | QA Tester & AI Consultant'
);
const accessForm = await driver.findElement(
By.id('accessForm')
);
assert.equal(await accessForm.isDisplayed(), true);
const firstName = await driver.findElement(
By.id('firstName')
);
assert.equal(await firstName.isDisplayed(), true);
Comprueba que los campos obligatorios muestren sus errores y que el portfolio permanezca oculto.
await driver.findElement(
By.id('accessButton')
).click();
const expectedErrors = {
firstNameError: 'El nombre es obligatorio.',
lastNameError: 'El apellido es obligatorio.',
emailError: 'El email es obligatorio.',
phoneError: 'El teléfono es obligatorio.',
consentError: 'Debés aceptar el registro de tus datos.'
};
for (const [errorId, expectedMessage] of Object.entries(expectedErrors)) {
const errorElement = await driver.findElement(By.id(errorId));
assert.equal(
await errorElement.getText(),
expectedMessage,
`Mensaje incorrecto en ${errorId}`
);
}
const portfolio = await driver.findElement(By.id('portfolio'));
assert.equal(await portfolio.isDisplayed(), false);
Valida que un correo con formato incorrecto impida el acceso al portfolio.
await driver.findElement(By.id('firstName'))
.sendKeys('Miguel');
await driver.findElement(By.id('lastName'))
.sendKeys('Flor');
await driver.findElement(By.id('email'))
.sendKeys('correo-invalido');
await driver.findElement(By.id('phone'))
.sendKeys('1123456789');
await driver.findElement(By.id('consent')).click();
await driver.findElement(By.id('accessButton')).click();
const emailError = await driver.findElement(
By.id('emailError')
);
assert.equal(
await emailError.getText(),
'Ingresá un email válido.',
'Debe mostrarse el mensaje de email inválido'
);
const portfolio = await driver.findElement(By.id('portfolio'));
assert.equal(
await portfolio.isDisplayed(),
false,
'El portfolio debe permanecer oculto'
);
Comprueba que un nombre que no cumple la longitud mínima sea rechazado.
await driver.findElement(By.id('firstName'))
.sendKeys('M');
await driver.findElement(By.id('lastName'))
.sendKeys('Flor');
await driver.findElement(By.id('email'))
.sendKeys('miguel@example.com');
await driver.findElement(By.id('phone'))
.sendKeys('1123456789');
await driver.findElement(By.id('consent')).click();
await driver.findElement(By.id('accessButton')).click();
const firstNameError = await driver.findElement(
By.id('firstNameError')
);
assert.equal(
await firstNameError.getText(),
'Ingresá un nombre válido.',
'Debe mostrarse el mensaje de nombre inválido'
);
const portfolio = await driver.findElement(By.id('portfolio'));
assert.equal(
await portfolio.isDisplayed(),
false,
'El portfolio debe permanecer oculto'
);
Verifica que un apellido inválido impida el acceso.
await driver.findElement(By.id('firstName'))
.sendKeys('Miguel');
await driver.findElement(By.id('lastName'))
.sendKeys('F');
await driver.findElement(By.id('email'))
.sendKeys('miguel@example.com');
await driver.findElement(By.id('phone'))
.sendKeys('1123456789');
await driver.findElement(By.id('consent')).click();
await driver.findElement(By.id('accessButton')).click();
const lastNameError = await driver.findElement(
By.id('lastNameError')
);
assert.equal(
await lastNameError.getText(),
'Ingresá un apellido válido.',
'Debe mostrarse el mensaje de apellido inválido'
);
const portfolio = await driver.findElement(By.id('portfolio'));
assert.equal(
await portfolio.isDisplayed(),
false,
'El portfolio debe permanecer oculto'
);
Verifica que el formulario rechace un teléfono que contiene letras, muestre el mensaje de validación correspondiente e impida el acceso al portfolio. Dato de prueba: 12345abc. Requisito: FR-008.
const caso = {
id: 'BUG-001',
descripcion: 'caracteres alfabéticos',
valor: '12345abc',
valido: false
};
await driver.findElement(By.id('phone'))
.sendKeys(caso.valor);
await driver.findElement(By.id('consent')).click();
await driver.findElement(By.id('accessButton')).click();
const phoneError = await driver.findElement(
By.id('phoneError')
);
const portfolio = await driver.findElement(
By.id('portfolio')
);
assert.equal(
await phoneError.getText(),
'Ingresá un teléfono válido.',
`Debe rechazarse ${caso.valor}`
);
assert.equal(
await portfolio.isDisplayed(),
false,
`El portfolio debe permanecer oculto para ${caso.valor}`
);
Archivo: phone-validation-boundaries.test.js. Escenario BUG-001: 3/3 PASS en Chrome, Firefox y Edge. Esta prueba forma parte de la suite telefónica de 33/33 PASS.
Verifica que el formulario rechace un nombre que contiene números, muestre el mensaje de validación correspondiente e impida el acceso al portfolio. Dato de prueba: sdfh567. Requisito: FR-012.
await driver.findElement(By.id('firstName'))
.sendKeys('sdfh567');
await driver.findElement(By.id('lastName'))
.sendKeys('Flor');
await driver.findElement(By.id('email'))
.sendKeys('miguel@example.com');
await driver.findElement(By.id('phone'))
.sendKeys('1123456789');
await driver.findElement(By.id('consent')).click();
await driver.findElement(By.id('accessButton')).click();
const firstNameError = await driver.findElement(
By.id('firstNameError')
);
assert.equal(
await firstNameError.getText(),
'Ingresá un nombre válido.'
);
const portfolio = await driver.findElement(
By.id('portfolio')
);
assert.equal(
await portfolio.isDisplayed(),
false
);
Verifica que el formulario rechace un apellido que contiene números, muestre el mensaje de validación correspondiente e impida el acceso al portfolio. Dato de prueba: Flor567. Requisito: FR-012.
await driver.findElement(By.id('firstName'))
.sendKeys('Miguel');
await driver.findElement(By.id('lastName'))
.sendKeys('Flor567');
await driver.findElement(By.id('email'))
.sendKeys('miguel@example.com');
await driver.findElement(By.id('phone'))
.sendKeys('1123456789');
await driver.findElement(By.id('consent')).click();
await driver.findElement(By.id('accessButton')).click();
const lastNameError = await driver.findElement(
By.id('lastNameError')
);
assert.equal(
await lastNameError.getText(),
'Ingresá un apellido válido.'
);
const portfolio = await driver.findElement(
By.id('portfolio')
);
assert.equal(
await portfolio.isDisplayed(),
false
);
Verifica que un teléfono de 16 dígitos, superior al máximo permitido de 15, muestre el mensaje de validación correspondiente e impida el acceso al portfolio. Requisito: FR-008. Caso relacionado: TC-018.
const caso = {
id: 'BUG-005',
descripcion: '16 dígitos',
valor: '1234567890123456',
valido: false
};
await driver.findElement(By.id('phone'))
.sendKeys(caso.valor);
await driver.findElement(By.id('consent')).click();
await driver.findElement(By.id('accessButton')).click();
const phoneError = await driver.findElement(
By.id('phoneError')
);
const portfolio = await driver.findElement(
By.id('portfolio')
);
assert.equal(
await phoneError.getText(),
'Ingresá un teléfono válido.',
`Debe rechazarse ${caso.valor}`
);
assert.equal(
await portfolio.isDisplayed(),
false,
`El portfolio debe permanecer oculto para ${caso.valor}`
);
Archivo: phone-validation-boundaries.test.js. Escenario BUG-005: 3/3 PASS en Chrome, Firefox y Edge. Suite telefónica completa: 33/33 PASS.
La suite incluye 11 escenarios de validación telefónica, ejecutados en Chrome, Firefox y Edge. Cada escenario obtuvo 3/3 PASS.
| Caso | Dato de prueba | Resultado esperado |
|---|---|---|
| PHONE-001 | 1234567 | Aceptado |
| PHONE-002 | 12345678 | Aceptado |
| PHONE-003 | 123456789012345 | Aceptado |
| BUG-005 | 1234567890123456 | Rechazado |
| BUG-001 | 12345abc | Rechazado |
| PHONE-004 | 11 2345 6789 | Aceptado |
| PHONE-005 | 11-2345-6789 | Aceptado |
| PHONE-006 | (11) 2345-6789 | Aceptado |
| PHONE-007 | +54 11 2345-6789 | Aceptado |
| PHONE-008 | 12345@6789 | Rechazado |
| PHONE-009 | +-() | Rechazado |
Comprueba que un teléfono de menos de siete dígitos sea rechazado.
await driver.findElement(By.id('firstName'))
.sendKeys('Miguel');
await driver.findElement(By.id('lastName'))
.sendKeys('Flor');
await driver.findElement(By.id('email'))
.sendKeys('miguel@example.com');
await driver.findElement(By.id('phone'))
.sendKeys('123456');
await driver.findElement(By.id('consent')).click();
await driver.findElement(By.id('accessButton')).click();
const phoneError = await driver.findElement(
By.id('phoneError')
);
assert.equal(
await phoneError.getText(),
'Ingresá un teléfono válido.',
'Debe mostrarse el mensaje de teléfono inválido'
);
const portfolio = await driver.findElement(By.id('portfolio'));
assert.equal(
await portfolio.isDisplayed(),
false,
'El portfolio debe permanecer oculto'
);
Confirma que el usuario no pueda acceder sin aceptar el registro de sus datos.
await driver.findElement(By.id('firstName'))
.sendKeys('Miguel');
await driver.findElement(By.id('lastName'))
.sendKeys('Flor');
await driver.findElement(By.id('email'))
.sendKeys('miguel@example.com');
await driver.findElement(By.id('phone'))
.sendKeys('1123456789');
const consent = await driver.findElement(By.id('consent'));
assert.equal(
await consent.isSelected(),
false,
'El consentimiento debe estar desmarcado inicialmente'
);
await driver.findElement(By.id('accessButton')).click();
const consentError = await driver.findElement(
By.id('consentError')
);
assert.equal(
await consentError.getText(),
'Debés aceptar el registro de tus datos.',
'Debe mostrarse el mensaje de consentimiento obligatorio'
);
const portfolio = await driver.findElement(By.id('portfolio'));
assert.equal(
await portfolio.isDisplayed(),
false,
'El portfolio debe permanecer oculto'
);
Comprueba que el registro válido permita ingresar y que el saludo muestre el nombre en mayúsculas.
await driver.findElement(By.id('firstName'))
.sendKeys('Miguel');
await driver.findElement(By.id('lastName'))
.sendKeys('Flor');
await driver.findElement(By.id('email'))
.sendKeys('selenium.test@example.com');
await driver.findElement(By.id('phone'))
.sendKeys('1123456789');
await driver.findElement(By.id('consent')).click();
await driver.findElement(By.id('accessButton')).click();
const portfolio = await driver.wait(
until.elementIsVisible(
await driver.findElement(By.id('portfolio'))
),
10000,
'El portfolio debería mostrarse después del registro válido.'
);
assert.equal(await portfolio.isDisplayed(), true);
assert.equal(
await driver.findElement(By.id('accessPage')).isDisplayed(),
false,
'El formulario de acceso debería quedar oculto.'
);
const welcomeMessage = await driver.findElement(
By.id('welcomeMessage')
);
assert.equal(
await welcomeMessage.getText(),
'Te doy la bienvenida, MIGUEL',
'El saludo debe mostrar únicamente el nombre en mayúsculas.'
);
Comprueba que el encabezado de acceso cambie entre español, inglés y portugués.
const languageSelect = await driver.findElement(
By.id('languageSelect')
);
const accessWelcome = await driver.findElement(
By.css('[data-i18n="accessWelcome"]')
);
const languages = [
{ code: 'es', expected: 'Te doy la bienvenida' },
{ code: 'en', expected: 'Welcome' },
{ code: 'pt', expected: 'Dou-lhe as boas-vindas' }
];
for (const language of languages) {
await new Select(languageSelect).selectByValue(language.code);
assert.equal(
await accessWelcome.getText(),
language.expected,
`El encabezado debería mostrarse en ${language.code}.`
);
}
Comprueba que un mensaje válido sea procesado mediante la API simulada, que aparezca la confirmación de envío, que el campo de mensaje quede vacío y que el botón vuelva a habilitarse. No se envían correos reales.
const message = await driver.findElement(
By.id('contactMessage')
);
await message.sendKeys(
'Prueba automatizada Selenium: consulta de contacto válida.'
);
const contactButton = await driver.findElement(
By.id('contactButton')
);
await contactButton.click();
const status = await driver.findElement(
By.id('contactStatus')
);
await driver.wait(
until.elementTextIs(
status,
'Mensaje enviado correctamente.'
),
10000
);
assert.equal(
await status.getText(),
'Mensaje enviado correctamente.'
);
assert.equal(
await message.getAttribute('value'),
''
);
assert.equal(
await contactButton.isEnabled(),
true
);
Comprueba que el formulario de contacto rechace un mensaje vacío mediante la validación HTML5. También verifica que no aparezca una confirmación de envío. No se comprueba directamente la ausencia de una solicitud HTTP.
const contactButton = await driver.findElement(
By.id('contactButton')
);
await contactButton.click();
const messageField = await driver.findElement(
By.id('contactMessage')
);
const isInvalid = await driver.executeScript(
'return !arguments[0].checkValidity();',
messageField
);
assert.equal(
isInvalid,
true,
'El campo vacío debe ser inválido según HTML5.'
);
const error = await driver.findElement(
By.id('contactMessageError')
);
const errorText = await error.getText();
assert.ok(
errorText === '' ||
errorText === 'El mensaje es obligatorio.',
'No debe aparecer un mensaje inesperado.'
);
const status = await driver.findElement(
By.id('contactStatus')
);
assert.equal(
await status.getText(),
'',
'No debe mostrarse una confirmación de envío.'
);
Comprueba que, ante un fallo simulado del servicio de contacto, el formulario muestre el mensaje de error correspondiente, conserve el texto ingresado y vuelva a habilitar el botón de envío. No se envían correos reales.
const message = await driver.findElement(
By.id('contactMessage')
);
const testMessage =
'Prueba Selenium: error HTTP 500 simulado.';
await message.sendKeys(testMessage);
const contactButton = await driver.findElement(
By.id('contactButton')
);
await contactButton.click();
const status = await driver.findElement(
By.id('contactStatus')
);
const expectedError =
'No se pudo enviar el mensaje. Intentá nuevamente más tarde.';
await driver.wait(
until.elementTextIs(status, expectedError),
10000
);
assert.equal(
await status.getText(),
expectedError
);
assert.equal(
await message.getAttribute('value'),
testMessage
);
assert.equal(
await contactButton.isEnabled(),
true
);
Pruebas funcionales y de regresión del buscador global: coincidencias, navegación, accesibilidad, internacionalización y comportamiento de los resultados.
RF-06 · HU-005 · BDD-006
Verifica que el buscador permita realizar consultas consecutivas, actualice los resultados sin recargar la página, elimine los resultados anteriores y cierre el panel al limpiar la búsqueda.
const escenarios = [
{ query: 'repositorio', destination: 'index.html#github' },
{ query: 'linkedin', destination: 'index.html#linkedin' },
{ query: 'marketing', destination: 'cv.html#education' }
];
for (const escenario of escenarios) {
await searchInput.clear();
await searchInput.sendKeys(escenario.query);
const resultado = await driver.wait(
until.elementLocated(
By.css(
`.portfolio-search-result[href="${escenario.destination}"]`
)
),
10000
);
assert.equal(await resultado.isDisplayed(), true);
assert.equal(
await searchInput.getAttribute('value'),
escenario.query
);
}
await searchInput.clear();
await driver.wait(async () => {
const panel = await driver.findElement(
By.css('.portfolio-search-results')
);
return !(await panel.isDisplayed());
}, 10000);
Fragmento representativo de la prueba real. El archivo original también verifica que no permanezcan resultados de consultas anteriores.
Verifica que el usuario pueda buscar una sección, seleccionar un resultado mediante la flecha hacia abajo y acceder a su destino presionando Enter.
await driver.get(
`${BASE_URL}/quality-management.html`
);
const searchInput = await driver.wait(
until.elementLocated(
By.css('.portfolio-search-input')
),
10000
);
// Buscar una sección conocida.
await searchInput.sendKeys('arquitectura');
// Esperar el resultado esperado.
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
),
10000
);
assert.equal(await resultado.isDisplayed(), true);
// Seleccionar el primer resultado con ArrowDown.
await searchInput.sendKeys('\uE015');
// Navegar con Enter.
await searchInput.sendKeys('\uE007');
// Verificar el destino.
await driver.wait(
until.urlContains('backend.html#arquitectura'),
10000
);
assert.ok(
(await driver.getCurrentUrl())
.endsWith('backend.html#arquitectura')
);
Fragmento de la prueba real que comprueba la selección mediante ArrowDown, la navegación con Enter y la llegada a la sección Arquitectura del backend.
Verifica que Escape cierre el panel de resultados, limpie el campo de búsqueda y actualice el estado de accesibilidad sin navegar a otra página. También comprueba que sea posible realizar una nueva búsqueda después del cierre.
await driver.get(`${BASE_URL}/quality-management.html`);
const searchInput = await driver.wait(
until.elementLocated(
By.css('.portfolio-search-input')
),
10000
);
const results = await driver.findElement(
By.css('.portfolio-search-results')
);
// Buscar y comprobar que aparecen resultados.
await searchInput.sendKeys('arquitectura');
await driver.wait(
until.elementIsVisible(results),
10000
);
assert.equal(await results.isDisplayed(), true);
// Cerrar mediante Escape.
await searchInput.sendKeys('\uE00C');
await driver.wait(
until.elementIsNotVisible(results),
10000
);
assert.equal(await results.isDisplayed(), false);
// Verificar limpieza del campo y accesibilidad.
assert.equal(
await searchInput.getAttribute('value'),
''
);
assert.equal(
await searchInput.getAttribute('aria-expanded'),
'false'
);
// Comprobar que no hubo navegación.
const currentUrl = await driver.getCurrentUrl();
assert.ok(
currentUrl.includes('quality-management.html')
);
// Realizar una nueva búsqueda.
await searchInput.sendKeys('persistencia');
await driver.wait(
until.elementIsVisible(results),
10000
);
const nuevoResultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#persistencia"]'
)
),
10000
);
assert.equal(await nuevoResultado.isDisplayed(), true);
// Verificar que el panel volvió a abrirse.
assert.equal(
await searchInput.getAttribute('aria-expanded'),
'true'
);
Fragmento de la prueba real que verifica el cierre con Escape, la limpieza del campo, la ausencia de navegación, los estados aria-expanded y la recuperación del buscador mediante una nueva consulta.
Comprueba que las búsquedas sin coincidencias muestren el mensaje correspondiente en español, inglés y portugués, sin generar enlaces de resultados.
const escenarios = [
{ language: 'es', expected: 'No se encontraron resultados.' },
{ language: 'en', expected: 'No results found.' },
{ language: 'pt', expected: 'Nenhum resultado encontrado.' }
];
for (const escenario of escenarios) {
await driver.executeScript(
'document.documentElement.lang = arguments[0]',
escenario.language
);
await searchInput.clear();
await searchInput.sendKeys('zzzxxyy123456');
await driver.wait(async () => {
const mensajes = await driver.findElements(
By.css('.portfolio-search-empty')
);
return mensajes.length > 0 &&
await mensajes[0].getText() === escenario.expected;
}, 10000);
const enlaces = await driver.findElements(
By.css('.portfolio-search-result')
);
assert.equal(enlaces.length, 0);
}
Fragmento de la prueba real que comprueba el mensaje localizado y la ausencia de enlaces para cada idioma.
Comprueba que una búsqueda encuentre la misma sección tanto si el usuario escribe palabras acentuadas como si omite los acentos.
const escenarios = [
'cierre de épica',
'cierre de epica'
];
for (const query of escenarios) {
await searchInput.clear();
await searchInput.sendKeys(query);
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="quality-management.html#cierre-epica"]'
)
),
10000
);
assert.equal(
await resultado.isDisplayed(),
true,
`No aparece el resultado para: ${query}`
);
}
Fragmento de la prueba real que verifica el mismo destino para las consultas con y sin acentos.
Verifica que el título y la descripción del resultado Arquitectura de la API coincidan exactamente con las traducciones esperadas en los tres idiomas.
const idiomas = ['es', 'en', 'pt'];
const traduccionesEsperadas = {
es: {
titulo: 'Arquitectura de la API',
descripcion: 'Endpoints implementados con Node.js y Express'
},
en: {
titulo: 'API architecture',
descripcion: 'Endpoints implemented with Node.js and Express'
},
pt: {
titulo: 'Arquitetura da API',
descripcion: 'Endpoints implementados com Node.js e Express'
}
};
for (const idioma of idiomas) {
await driver.executeScript(
'document.documentElement.lang = arguments[0]',
idioma
);
await searchInput.clear();
await searchInput.sendKeys('arquitectura');
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
),
10000
);
const titulo = await resultado.findElement(
By.css('strong')
).getText();
const descripcion = await resultado.findElement(
By.css('small')
).getText();
assert.ok(titulo.trim().length > 0);
assert.ok(descripcion.trim().length > 0);
assert.equal(
titulo,
traduccionesEsperadas[idioma].titulo
);
assert.equal(
descripcion,
traduccionesEsperadas[idioma].descripcion
);
}
Fragmento de la prueba real que compara títulos y descripciones con los textos esperados en ES, EN y PT.
Verifica que el panel se cierre al hacer clic fuera del buscador, conserve la consulta y vuelva a mostrar los resultados al recuperar el foco.
await searchInput.sendKeys('arquitectura');
await driver.wait(
until.elementIsVisible(results),
10000
);
const zonaExterna = await driver.findElement(
By.css('h1')
);
await zonaExterna.click();
await driver.wait(
until.elementIsNotVisible(results),
10000
);
assert.equal(await results.isDisplayed(), false);
assert.equal(
await searchInput.getAttribute('value'),
'arquitectura',
'El clic externo eliminó el texto de búsqueda'
);
await searchInput.click();
await driver.wait(
until.elementIsVisible(results),
10000
);
const resultado = await driver.findElement(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
);
assert.equal(await resultado.isDisplayed(), true);
Fragmento de la prueba real que verifica el cierre mediante clic externo, la conservación del texto y la reapertura al recuperar el foco.
Comprueba el orden de los resultados de QA y la navegación entre ellos mediante las flechas del teclado, seguida de la apertura con Enter.
await searchInput.sendKeys('qa');
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length >= 2;
}, 10000);
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
const primero = resultados[0];
const segundo = resultados[1];
assert.match(
await primero.getText(),
/evidencia verificable/i
);
assert.match(
await segundo.getText(),
/especialidades/i
);
await searchInput.sendKeys('\uE015');
await searchInput.sendKeys('\uE015');
await searchInput.sendKeys('\uE013');
await searchInput.sendKeys('\uE007');
await driver.wait(
until.urlContains('qa.html'),
10000
);
const currentUrl = await driver.getCurrentUrl();
assert.ok(currentUrl.includes('qa.html'));
Fragmento de la prueba real que utiliza ArrowDown, ArrowUp y Enter para seleccionar y abrir el resultado correspondiente a QA.
Verifica que el usuario pueda desplazar el foco hasta el segundo resultado, abrirlo con Enter y llegar a la sección Especialidades.
await searchInput.sendKeys('qa');
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length >= 2;
}, 10000);
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
assert.match(
await resultados[1].getText(),
/especialidades/i
);
await searchInput.sendKeys('\uE015');
await driver.wait(async () => {
return await driver.executeScript(`
return document.activeElement?.id ===
'portfolio-search-result-0';
`);
}, 10000);
const primerResultado = await driver.switchTo().activeElement();
await primerResultado.sendKeys('\uE015');
await driver.wait(async () => {
return await driver.executeScript(`
return document.activeElement?.id ===
'portfolio-search-result-1';
`);
}, 10000);
const segundoResultado = await driver.switchTo().activeElement();
await segundoResultado.sendKeys('\uE007');
await driver.wait(
until.urlContains('index.html#specialties'),
10000
);
const currentUrl = await driver.getCurrentUrl();
assert.ok(
currentUrl.endsWith('index.html#specialties')
);
Fragmento de la prueba real que verifica el foco del primer y segundo resultado y la navegación exacta a index.html#specialties.
Comprueba que los espacios al inicio y al final de una consulta no impidan encontrar un resultado ni navegar correctamente a su destino.
await searchInput.sendKeys(' arquitectura ');
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
),
10000
);
assert.equal(
await resultado.isDisplayed(),
true,
'El resultado de arquitectura no aparece'
);
await resultado.click();
await driver.wait(
until.urlContains('backend.html#arquitectura'),
10000
);
const currentUrl = await driver.getCurrentUrl();
assert.ok(
currentUrl.endsWith('backend.html#arquitectura')
);
Fragmento de la prueba real que comprueba la búsqueda con espacios adicionales y la navegación al destino exacto de Arquitectura del backend.
Verifica que arquitectura, ARQUITECTURA y ArQuItEcTuRa encuentren el mismo resultado.
const consultas = [
'arquitectura',
'ARQUITECTURA',
'ArQuItEcTuRa'
];
for (const consulta of consultas) {
// Limpiar la consulta anterior.
await searchInput.clear();
// Ingresar la nueva consulta.
await searchInput.sendKeys(consulta);
// Esperar el resultado correspondiente.
const resultado = await driver.wait(async () => {
const elementos = await driver.findElements(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
);
if (elementos.length === 0) return false;
return await elementos[0].isDisplayed()
? elementos[0]
: false;
}, 10000);
assert.equal(
await resultado.isDisplayed(),
true,
`No se encontró arquitectura con: ${consulta}`
);
}
Compara la visibilidad del destino esperado para tres variantes de capitalización.
Comprueba que borrar toda la consulta cierre los resultados, deje el campo vacío y cambie aria-expanded a false.
// 1. Realizar una búsqueda válida.
await searchInput.sendKeys('arquitectura');
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length > 0 &&
await resultados[0].isDisplayed();
}, 10000);
assert.equal(
await searchInput.getAttribute('aria-expanded'),
'true'
);
// 2. Borrar completamente la consulta.
await searchInput.clear();
// 3. Esperar el cierre del desplegable.
await driver.wait(async () => {
const expanded = await searchInput.getAttribute('aria-expanded');
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
const visibles = [];
for (const resultado of resultados) {
if (await resultado.isDisplayed()) {
visibles.push(resultado);
}
}
return expanded === 'false' && visibles.length === 0;
}, 10000);
// 4. Verificar el campo vacío.
assert.equal(
await searchInput.getAttribute('value'),
''
);
Verifica la ausencia de resultados visibles y el estado de accesibilidad después de limpiar el campo.
Comprueba que una consulta compuesta por caracteres especiales muestre el mensaje sin resultados en español y no genere enlaces seleccionables.
// 1. Establecer el idioma español.
await driver.executeScript(`
document.documentElement.lang = 'es';
`);
// 2. Introducir caracteres especiales.
await searchInput.sendKeys('@@@###%%%');
// 3. Esperar el mensaje sin resultados.
const mensaje = await driver.wait(
until.elementLocated(By.css('.portfolio-search-empty')),
10000
);
await driver.wait(
until.elementIsVisible(mensaje),
10000
);
assert.equal(
(await mensaje.getText()).trim(),
'No se encontraron resultados.'
);
// 4. Comprobar que no existen resultados seleccionables.
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
assert.equal(
resultados.length,
0,
'No deberían existir resultados para esta consulta'
);
Verifica el mensaje esperado y que la lista de enlaces permanezca vacía.
Verifica que al sustituir arquitectura por linkedin aparezca el nuevo resultado, desaparezca el anterior y se conserve la consulta actual.
// 1. Buscar arquitectura.
await searchInput.sendKeys('arquitectura');
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
);
return resultados.length > 0 && await resultados[0].isDisplayed();
}, 10000);
// 2. Sustituir la consulta sin recargar.
await searchInput.clear();
await searchInput.sendKeys('linkedin');
// 3. Esperar el resultado de LinkedIn.
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css(
'.portfolio-search-result[href="index.html#linkedin"]'
)
);
return resultados.length > 0 && await resultados[0].isDisplayed();
}, 10000);
// 4. Comprobar que desapareció el resultado anterior.
const resultadosAnteriores = await driver.findElements(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
);
assert.equal(
resultadosAnteriores.length,
0,
'La búsqueda anterior continúa en los resultados'
);
// 5. Verificar la consulta actual.
assert.equal(
await searchInput.getAttribute('value'),
'linkedin'
);
Comprueba la actualización de resultados sin recargar la página y la eliminación de coincidencias anteriores.
Comprueba que el segundo resultado de QA sea Especialidades y que al hacer clic se abra la sección con el ancla correcta.
// 1. Buscar QA.
await searchInput.sendKeys('qa');
// 2. Esperar el resultado Especialidades.
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="index.html#specialties"]'
)
),
10000
);
await driver.wait(
until.elementIsVisible(resultado),
10000
);
// 3. Verificar que es el segundo resultado.
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
assert.equal(
await resultados[1].getAttribute('href'),
`${BASE_URL}/index.html#specialties`
);
// 4. Seleccionar Especialidades mediante clic.
await resultado.click();
// 5. Verificar la navegación.
await driver.wait(
until.urlContains('index.html#specialties'),
10000
);
const currentUrl = await driver.getCurrentUrl();
assert.ok(
currentUrl.endsWith('index.html#specialties'),
`Destino incorrecto: ${currentUrl}`
);
// 6. Verificar que existe la sección de destino.
const seccion = await driver.wait(
until.elementLocated(By.id('specialties')),
10000
);
assert.ok(
seccion,
'No se encontró la sección Especialidades'
);
// 7. Verificar que la URL conserva el ancla correcta.
const hash = await driver.executeScript(
'return window.location.hash;'
);
assert.equal(
hash,
'#specialties',
'El ancla de navegación es incorrecta'
);
Verifica posición, URL, existencia de la sección y hash #specialties.
Verifica que el buscador funcione tras navegar de Gestión de calidad a Backend y permita regresar mediante otra búsqueda.
// 1. Abrir Gestión de calidad.
await driver.get(`${BASE_URL}/quality-management.html`);
let searchInput = await driver.wait(
until.elementLocated(By.css('.portfolio-search-input')),
10000
);
// 2. Buscar arquitectura.
await searchInput.sendKeys('arquitectura');
const arquitectura = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
),
10000
);
await driver.wait(
until.elementIsVisible(arquitectura),
10000
);
// 3. Navegar a Backend.
await arquitectura.click();
await driver.wait(
until.urlContains('backend.html#arquitectura'),
10000
);
// 4. Localizar el buscador en la nueva página.
searchInput = await driver.wait(
until.elementLocated(By.css('.portfolio-search-input')),
10000
);
// 5. Realizar una nueva búsqueda.
await searchInput.sendKeys('trazabilidad');
// 6. Verificar el resultado.
const trazabilidad = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="quality-management.html#trazabilidad"]'
)
),
10000
);
await driver.wait(
until.elementIsVisible(trazabilidad),
10000
);
// 7. Navegar nuevamente a Gestión de calidad.
await trazabilidad.click();
await driver.wait(
until.urlContains('quality-management.html#trazabilidad'),
10000
);
const currentUrl = await driver.getCurrentUrl();
assert.ok(
currentUrl.endsWith('quality-management.html#trazabilidad'),
`Destino incorrecto: ${currentUrl}`
);
Comprueba las dos navegaciones y la disponibilidad del buscador en la nueva página.
Comprueba que la búsqueda QA presente primero la página QA y después la sección Especialidades.
// 1. Buscar QA.
await searchInput.sendKeys('qa');
// 2. Esperar al menos dos resultados visibles.
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length >= 2 &&
await resultados[0].isDisplayed() &&
await resultados[1].isDisplayed();
}, 10000);
// 3. Recuperar los resultados en orden.
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
// 4. Verificar el primer destino.
assert.ok(
(await resultados[0].getAttribute('href'))
.endsWith('/qa.html'),
'El primer resultado debería ser QA'
);
// 5. Verificar el segundo destino.
assert.ok(
(await resultados[1].getAttribute('href'))
.endsWith('/index.html#specialties'),
'El segundo resultado debería ser Especialidades'
);
Verifica el orden de los dos primeros destinos mediante sus enlaces.
Verifica que Tab enfoque el primer resultado y Enter lo abra; contempla el foco intermedio en el contenedor desplazable de Firefox.
// 1. Buscar QA.
await searchInput.sendKeys('qa');
// 2. Esperar el primer resultado.
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length > 0 &&
await resultados[0].isDisplayed();
}, 10000);
// 3. Navegar con Tab desde el buscador.
await searchInput.sendKeys('\uE004');
let elementoActivo = await driver.switchTo().activeElement();
// Firefox puede enfocar primero el contenedor desplazable.
const esContenedor = await driver.executeScript(`
return document.activeElement?.id ===
'portfolio-search-results';
`);
if (esContenedor) {
await elementoActivo.sendKeys('\uE004');
}
// 4. Verificar que Tab llega al primer resultado.
await driver.wait(async () => {
return await driver.executeScript(`
return document.activeElement?.classList.contains(
'portfolio-search-result'
) === true;
`);
}, 10000);
const resultadoActivo = await driver.switchTo().activeElement();
assert.ok(
(await resultadoActivo.getAttribute('href'))
.endsWith('/qa.html'),
'Tab no seleccionó el primer resultado esperado'
);
// 5. Abrir el resultado con Enter.
await resultadoActivo.sendKeys('\uE007');
// 6. Verificar la navegación.
await driver.wait(
until.urlContains('/qa.html'),
10000
);
const currentUrl = await driver.getCurrentUrl();
assert.ok(
currentUrl.endsWith('/qa.html'),
`Destino incorrecto: ${currentUrl}`
);
Comprueba el foco, el destino QA y el comportamiento específico contemplado para Firefox.
Verifica que Tab avance hasta el segundo resultado, Shift+Tab regrese al primero y Enter abra la página QA.
// 1. Buscar QA.
await searchInput.sendKeys('qa');
// 2. Esperar al menos dos resultados.
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length >= 2 &&
await resultados[1].isDisplayed();
}, 10000);
// 3. Avanzar al primer resultado con Tab.
await searchInput.sendKeys('\uE004');
let elementoActivo = await driver.switchTo().activeElement();
// Firefox puede enfocar primero el contenedor desplazable.
const esContenedor = await driver.executeScript(`
return document.activeElement?.id ===
'portfolio-search-results';
`);
if (esContenedor) {
await elementoActivo.sendKeys('\uE004');
}
// Verificar que el foco llegue al primer resultado.
await driver.wait(async () => {
return await driver.executeScript(`
return document.activeElement?.id ===
'portfolio-search-result-0';
`);
}, 10000);
// 4. Avanzar al segundo resultado con Tab.
let resultadoActivo = await driver.switchTo().activeElement();
await resultadoActivo.sendKeys('\uE004');
await driver.wait(async () => {
return await driver.executeScript(`
return document.activeElement?.id ===
'portfolio-search-result-1';
`);
}, 10000);
// 5. Regresar al primero con Shift + Tab.
resultadoActivo = await driver.switchTo().activeElement();
await resultadoActivo.sendKeys('\uE008', '\uE004');
await driver.wait(async () => {
return await driver.executeScript(`
return document.activeElement?.id ===
'portfolio-search-result-0';
`);
}, 10000);
// 6. Abrir el primer resultado con Enter.
resultadoActivo = await driver.switchTo().activeElement();
await resultadoActivo.sendKeys('\uE007');
await driver.wait(
until.urlContains('/qa.html'),
10000
);
assert.ok(
(await driver.getCurrentUrl()).endsWith('/qa.html'),
'La navegación no llegó a QA'
);
Comprueba el foco en ambos resultados y el regreso al primero antes de navegar.
Verifica el recorrido del primer al tercer resultado con ArrowDown y el regreso al primero con ArrowUp, sin navegar.
// 1. Buscar QA.
await searchInput.sendKeys('qa');
// 2. Esperar al menos tres resultados.
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length >= 3 &&
await resultados[2].isDisplayed();
}, 10000);
// 3. Seleccionar el primer resultado.
await searchInput.sendKeys('\uE015');
const verificarFoco = async (indice) => {
await driver.wait(async () => {
return await driver.executeScript(
'return document.activeElement?.id;',
) === `portfolio-search-result-${indice}`;
}, 10000);
};
await verificarFoco(0);
// 4. Avanzar al segundo resultado.
let activo = await driver.switchTo().activeElement();
await activo.sendKeys('\uE015');
await verificarFoco(1);
// 5. Avanzar al tercer resultado.
activo = await driver.switchTo().activeElement();
await activo.sendKeys('\uE015');
await verificarFoco(2);
// 6. Regresar al segundo resultado.
activo = await driver.switchTo().activeElement();
await activo.sendKeys('\uE013');
await verificarFoco(1);
// 7. Regresar al primer resultado.
activo = await driver.switchTo().activeElement();
await activo.sendKeys('\uE013');
await verificarFoco(0);
// 8. Verificar que seguimos en la misma página.
assert.ok(
(await driver.getCurrentUrl())
.includes('/quality-management.html'),
'La navegación cambió de página inesperadamente'
);
Comprueba cada cambio de foco y que la URL permanezca en Gestión de calidad.
Verifica que Escape cierre los resultados cuando el foco está en un enlace, actualice aria-expanded y no provoque navegación.
// 1. Buscar QA.
await searchInput.sendKeys('qa');
// 2. Esperar resultados.
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length >= 2 &&
await resultados[0].isDisplayed();
}, 10000);
// 3. Seleccionar el primer resultado con ArrowDown.
await searchInput.sendKeys('\uE015');
await driver.wait(async () => {
return await driver.executeScript(`
return document.activeElement?.id ===
'portfolio-search-result-0';
`);
}, 10000);
// 4. Presionar Escape desde el resultado seleccionado.
const resultadoActivo = await driver.switchTo().activeElement();
await resultadoActivo.sendKeys('\uE00C');
// 5. Verificar el cierre del desplegable.
await driver.wait(async () => {
return await searchInput.getAttribute('aria-expanded') === 'false';
}, 10000);
// 6. Verificar que no quedan resultados visibles.
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
for (const resultado of resultados) {
assert.equal(
await resultado.isDisplayed(),
false,
'El desplegable conserva resultados visibles'
);
}
// 7. Verificar que no hubo navegación.
assert.ok(
(await driver.getCurrentUrl())
.includes('/quality-management.html'),
'Escape provocó una navegación inesperada'
);
Comprueba el cierre desde un resultado enfocado, la ausencia de enlaces visibles y la permanencia en la página.
Verifica que, tras cerrar los resultados con Escape desde un enlace seleccionado, el buscador permita escribir otra consulta, muestre el resultado de Persistencia y actualice aria-expanded.
// 1. Buscar QA.
await searchInput.sendKeys('qa');
// 2. Esperar el primer resultado.
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length > 0 &&
await resultados[0].isDisplayed();
}, 10000);
// 3. Seleccionar el primer resultado.
await searchInput.sendKeys('\uE015');
await driver.wait(async () => {
return await driver.executeScript(`
return document.activeElement?.id ===
'portfolio-search-result-0';
`);
}, 10000);
// 4. Cerrar mediante Escape desde el resultado.
const activo = await driver.switchTo().activeElement();
await activo.sendKeys('\uE00C');
await driver.wait(async () => {
return await searchInput.getAttribute('aria-expanded') === 'false';
}, 10000);
// 5. Volver al campo de búsqueda y escribir otra consulta.
await searchInput.clear();
await searchInput.sendKeys('persistencia');
// 6. Comprobar que aparece el nuevo resultado.
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#persistencia"]'
)
),
10000
);
await driver.wait(
until.elementIsVisible(resultado),
10000
);
assert.equal(
await searchInput.getAttribute('aria-expanded'),
'true'
);
assert.equal(
await searchInput.getAttribute('value'),
'persistencia'
);
Fragmento real que comprueba el cierre con Escape desde un resultado y la recuperación de una nueva búsqueda.
Comprueba que aria-expanded sea false inicialmente, cambie a true al mostrar resultados, vuelva a false al limpiar y regrese a true con otra búsqueda.
// 1. Verificar el estado inicial.
assert.equal(
await searchInput.getAttribute('aria-expanded'),
'false',
'El buscador debería comenzar cerrado'
);
// 2. Introducir una consulta válida.
await searchInput.sendKeys('arquitectura');
await driver.wait(async () => {
return await searchInput.getAttribute('aria-expanded') === 'true';
}, 10000);
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
assert.ok(
resultados.length > 0,
'No aparecen resultados al abrir el buscador'
);
// 3. Limpiar la consulta.
await searchInput.clear();
await driver.wait(async () => {
return await searchInput.getAttribute('aria-expanded') === 'false';
}, 10000);
// 4. Realizar otra búsqueda.
await searchInput.sendKeys('persistencia');
await driver.wait(async () => {
return await searchInput.getAttribute('aria-expanded') === 'true';
}, 10000);
Fragmento real que verifica la secuencia de estados de accesibilidad del buscador.
Verifica que cada resultado de la búsqueda QA tenga un enlace no vacío, texto visible y un destino dentro del portfolio.
// 1. Realizar la búsqueda.
await searchInput.sendKeys('qa');
// 2. Esperar resultados visibles.
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length > 0 &&
await resultados[0].isDisplayed();
}, 10000);
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
// 3. Verificar cada resultado.
for (const [indice, resultado] of resultados.entries()) {
const href = await resultado.getAttribute('href');
const texto = (await resultado.getText()).trim();
assert.ok(
href && href.trim().length > 0,
`El resultado ${indice + 1} no tiene enlace`
);
assert.ok(
texto.length > 0,
`El resultado ${indice + 1} no tiene texto visible`
);
assert.ok(
href.startsWith(BASE_URL),
`El resultado ${indice + 1} apunta fuera del portfolio: ${href}`
);
}
Fragmento real que recorre todos los resultados y valida sus enlaces y textos.
Comprueba que los resultados de la búsqueda QA no contengan enlaces que apunten al mismo destino.
// 1. Buscar QA.
await searchInput.sendKeys('qa');
// 2. Esperar resultados visibles.
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length > 0 &&
await resultados[0].isDisplayed();
}, 10000);
// 3. Recuperar los destinos.
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
const destinos = [];
for (const resultado of resultados) {
destinos.push(await resultado.getAttribute('href'));
}
// 4. Comparar destinos originales y únicos.
const destinosUnicos = new Set(destinos);
assert.equal(
destinosUnicos.size,
destinos.length,
'Se encontraron destinos duplicados en los resultados'
);
Fragmento real que compara la cantidad de destinos con la cantidad de destinos únicos.
Comprueba que, al sustituir una búsqueda válida por otra sin coincidencias, desaparezcan los enlaces anteriores y aparezca el mensaje en español.
// 1. Establecer español.
await driver.executeScript(
"document.documentElement.lang = 'es';"
);
// 2. Realizar una búsqueda con resultados.
await searchInput.sendKeys('qa');
await driver.wait(async () => {
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
return resultados.length > 0 &&
await resultados[0].isDisplayed();
}, 10000);
// 3. Sustituir por una consulta sin coincidencias.
await searchInput.clear();
await searchInput.sendKeys('zzzxxyy123456');
// 4. Esperar el mensaje sin resultados.
await driver.wait(async () => {
const mensajes = await driver.findElements(
By.css('.portfolio-search-empty')
);
return mensajes.length > 0 &&
await mensajes[0].isDisplayed();
}, 10000);
// 5. Comprobar el mensaje.
const mensaje = await driver.findElement(
By.css('.portfolio-search-empty')
);
assert.equal(
(await mensaje.getText()).trim(),
'No se encontraron resultados.'
);
// 6. Verificar que desaparecieron los enlaces anteriores.
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
assert.equal(
resultados.length,
0,
'Permanecen resultados de la búsqueda anterior'
);
Fragmento real que verifica la eliminación de resultados previos y el mensaje sin coincidencias.
Verifica que, después de una consulta sin coincidencias, una búsqueda válida vuelva a mostrar Arquitectura, oculte el mensaje anterior y abra el panel.
// 1. Buscar un término inexistente.
await searchInput.sendKeys('zzzxxyy123456');
await driver.wait(async () => {
const mensajes = await driver.findElements(
By.css('.portfolio-search-empty')
);
return mensajes.length > 0 &&
await mensajes[0].isDisplayed();
}, 10000);
// 2. Sustituir la consulta por una válida.
await searchInput.clear();
await searchInput.sendKeys('arquitectura');
// 3. Esperar el resultado correspondiente.
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
),
10000
);
await driver.wait(
until.elementIsVisible(resultado),
10000
);
// 4. Verificar que desapareció el mensaje anterior.
const mensajes = await driver.findElements(
By.css('.portfolio-search-empty')
);
for (const mensaje of mensajes) {
assert.equal(
await mensaje.isDisplayed(),
false,
'El mensaje sin resultados continúa visible'
);
}
// 5. Verificar que el desplegable está abierto.
assert.equal(
await searchInput.getAttribute('aria-expanded'),
'true'
);
Fragmento real que comprueba la recuperación del buscador tras una consulta sin resultados.
Comprueba que la consulta parcial arquitect encuentre el resultado Arquitectura y mantenga abierto el panel de búsqueda.
// 1. Buscar utilizando parte del término.
await searchInput.sendKeys('arquitect');
// 2. Esperar el resultado esperado.
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
),
10000
);
await driver.wait(
until.elementIsVisible(resultado),
10000
);
// 3. Verificar el destino.
assert.ok(
(await resultado.getAttribute('href'))
.endsWith('/backend.html#arquitectura'),
'La búsqueda parcial no encontró Arquitectura'
);
// 4. Verificar el estado del desplegable.
assert.equal(
await searchInput.getAttribute('aria-expanded'),
'true'
);
Fragmento real que verifica el destino encontrado y aria-expanded para una consulta parcial.
Verifica que el término Express encuentre Arquitectura mediante el texto de su descripción y que el enlace apunte al destino esperado.
// 1. Buscar un término presente en la descripción.
await searchInput.sendKeys('Express');
// 2. Esperar el resultado de Arquitectura.
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
),
10000
);
await driver.wait(
until.elementIsVisible(resultado),
10000
);
// 3. Verificar que el término aparece en la descripción.
const descripcion = await resultado.getText();
assert.match(
descripcion,
/express/i,
'El término no aparece en el resultado encontrado'
);
// 4. Verificar el destino.
assert.ok(
(await resultado.getAttribute('href'))
.endsWith('/backend.html#arquitectura'),
'El resultado no corresponde a Arquitectura'
);
Fragmento real que comprueba el texto del resultado y el destino al buscar por descripción.
Comprueba que la consulta arquitectura api encuentre Arquitectura de la API y que el panel permanezca abierto.
// 1. Buscar utilizando varias palabras.
await searchInput.sendKeys('arquitectura api');
// 2. Esperar el resultado correspondiente.
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
),
10000
);
await driver.wait(
until.elementIsVisible(resultado),
10000
);
// 3. Verificar el destino.
assert.ok(
(await resultado.getAttribute('href'))
.endsWith('/backend.html#arquitectura'),
'La búsqueda no encontró Arquitectura de la API'
);
// 4. Verificar que el desplegable está abierto.
assert.equal(
await searchInput.getAttribute('aria-expanded'),
'true'
);
Fragmento real que verifica la coincidencia y el estado del panel para una búsqueda de varias palabras.
Verifica que la consulta arquitectura api, con varios espacios entre palabras, encuentre Arquitectura y mantenga el panel abierto.
// 1. Buscar con varios espacios entre palabras.
await searchInput.sendKeys('arquitectura api');
// 2. Esperar el resultado de Arquitectura.
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
),
10000
);
await driver.wait(
until.elementIsVisible(resultado),
10000
);
// 3. Verificar que el resultado corresponde al destino esperado.
assert.ok(
(await resultado.getAttribute('href'))
.endsWith('/backend.html#arquitectura'),
'La búsqueda con espacios internos no encontró Arquitectura'
);
// 4. Verificar el estado del desplegable.
assert.equal(
await searchInput.getAttribute('aria-expanded'),
'true'
);
Fragmento real que comprueba la normalización de espacios internos y el destino del resultado.
Verifica que dos tabulaciones entre arquitectura y api se conserven en el campo, permitan encontrar Arquitectura y mantengan el panel abierto.
// 1. Introducir una consulta con tabulaciones.
// Se asigna el valor directamente para evitar que
// Selenium interprete TAB como navegación de foco.
await driver.executeScript(
`
const input = arguments[0];
input.value = "arquitectura\\t\\tapi";
input.dispatchEvent(new Event("input", { bubbles: true }));
`,
searchInput
);
// 2. Verificar que el valor contiene dos tabulaciones.
const valor = await searchInput.getAttribute('value');
assert.equal(
valor,
'arquitectura\t\tapi',
'El campo no contiene las tabulaciones esperadas'
);
// 3. Esperar el resultado de Arquitectura.
const resultado = await driver.wait(
until.elementLocated(
By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)
),
10000
);
await driver.wait(
until.elementIsVisible(resultado),
10000
);
// 4. Verificar el destino.
assert.ok(
(await resultado.getAttribute('href'))
.endsWith('/backend.html#arquitectura'),
'La búsqueda con tabulaciones no encontró Arquitectura'
);
// 5. Verificar el estado del buscador.
assert.equal(
await searchInput.getAttribute('aria-expanded'),
'true'
);
Fragmento real que asigna las tabulaciones mediante JavaScript, dispara el evento input y comprueba valor, resultado y aria-expanded.
Comprueba que una consulta con un acento Unicode combinante encuentre la sección Internacionalización y mantenga abierto el panel.
const consulta = 'internacionalizacio\u0301n';
await searchInput.sendKeys(consulta);
const resultado = await driver.wait(
until.elementLocated(By.css(
'.portfolio-search-result[href="frontend.html#internacionalizacion"]'
)), 10000
);
await driver.wait(until.elementIsVisible(resultado), 10000);
assert.ok(
(await resultado.getAttribute('href'))
.endsWith('/frontend.html#internacionalizacion')
);
assert.equal(
await searchInput.getAttribute('aria-expanded'), 'true'
);
Fragmento de la prueba real que utiliza Unicode descompuesto, verifica el destino de frontend.html y comprueba aria-expanded.
Verifica que el campo HTML elimine un salto de línea, produzca el valor arquitecturaapi y muestre el mensaje sin resultados con el panel abierto.
const valorReal = await driver.executeScript(`
const input = arguments[0];
input.value = "arquitectura\\napi";
input.dispatchEvent(new Event("input", { bubbles: true }));
return input.value;
`, searchInput);
assert.equal(valorReal, 'arquitecturaapi');
const mensaje = await driver.wait(
until.elementLocated(By.css('.portfolio-search-empty')), 10000
);
await driver.wait(until.elementIsVisible(mensaje), 10000);
assert.match(
await mensaje.getText(), /No se encontraron resultados/i
);
assert.equal(
await searchInput.getAttribute('aria-expanded'), 'true'
);
Fragmento de la prueba real que asigna el valor mediante JavaScript, dispara el evento input y comprueba el valor sanitizado y el mensaje.
Comprueba que una consulta de cinco espacios conserve su valor en el campo, pero se trate como vacía, sin resultados y con el panel oculto.
await searchInput.sendKeys(' ');
assert.equal(
await searchInput.getAttribute('value'), ' '
);
assert.equal(
await searchInput.getAttribute('aria-expanded'), 'false'
);
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
assert.equal(resultados.length, 0);
const contenedor = await driver.findElement(
By.css('.portfolio-search-results')
);
assert.equal(await contenedor.getAttribute('hidden'), 'true');
Fragmento de la prueba real que verifica el valor del campo, aria-expanded, la ausencia de enlaces y el atributo hidden.
Verifica que agregar símbolos a una consulta válida no produzca coincidencias incorrectas y muestre el mensaje sin resultados.
await searchInput.sendKeys('arquitectura@@@');
const mensaje = await driver.wait(
until.elementLocated(By.css('.portfolio-search-empty')), 10000
);
await driver.wait(until.elementIsVisible(mensaje), 10000);
assert.match(
await mensaje.getText(), /No se encontraron resultados/i
);
const resultados = await driver.findElements(
By.css('.portfolio-search-result')
);
assert.equal(resultados.length, 0);
assert.equal(
await searchInput.getAttribute('aria-expanded'), 'true'
);
Fragmento de la prueba real que busca arquitectura@@@, comprueba la ausencia de enlaces y verifica aria-expanded.
Verifica que, después de una consulta sin coincidencias con símbolos, una nueva búsqueda válida recupere resultados y elimine el mensaje anterior sin recargar.
await searchInput.sendKeys('arquitectura@@@');
const mensaje = await driver.wait(
until.elementLocated(By.css('.portfolio-search-empty')), 10000
);
await driver.wait(until.elementIsVisible(mensaje), 10000);
assert.match(
await mensaje.getText(), /No se encontraron resultados/i
);
await searchInput.clear();
await searchInput.sendKeys('arquitectura');
const resultado = await driver.wait(
until.elementLocated(By.css(
'.portfolio-search-result[href="backend.html#arquitectura"]'
)), 10000
);
await driver.wait(until.elementIsVisible(resultado), 10000);
const mensajesVacios = await driver.findElements(
By.css('.portfolio-search-empty')
);
assert.equal(mensajesVacios.length, 0);
assert.equal(
await searchInput.getAttribute('aria-expanded'), 'true'
);
Fragmento de la prueba real que sustituye arquitectura@@@ por arquitectura, comprueba el resultado y verifica que desaparezca el mensaje anterior.
Verifica que la búsqueda de «arquitectura» permita acceder a backend.html#arquitectura, que la sección de destino sea visible y que no quede oculta detrás de la cabecera fija.
const caso = {
id: 'SEARCH-TC-038',
query: 'arquitectura',
destination: 'backend.html#arquitectura',
sectionId: 'arquitectura'
};
// Fragmento del test parametrizado compartido.
await searchInput.sendKeys(caso.query);
const result = await driver.wait(
until.elementLocated(
By.css(`.portfolio-search-result[href="${caso.destination}"]`)
),
10000
);
assert.equal(await result.isDisplayed(), true);
await result.click();
await driver.wait(until.urlContains(caso.destination), 10000);
assert.ok((await driver.getCurrentUrl()).endsWith(caso.destination));
const section = await driver.findElement(By.id(caso.sectionId));
assert.equal(await section.isDisplayed(), true);
const posicion = await driver.executeScript(`
const section = arguments[0];
const navbar = document.querySelector('.navbar');
return {
sectionTop: section.getBoundingClientRect().top,
navbarBottom: navbar ? navbar.getBoundingClientRect().bottom : 0
};
`, section);
assert.ok(posicion.sectionTop >= posicion.navbarBottom - 2);
Fragmento representativo del test parametrizado compartido; se muestran los datos de SEARCH-TC-038 y las comprobaciones de resultado, URL, sección y cabecera.
Verifica que la búsqueda de «casos de prueba» permita acceder a qa.html#casos-prueba, que la sección de destino sea visible y que no quede oculta detrás de la cabecera fija.
const caso = {
id: 'SEARCH-TC-040',
query: 'casos de prueba',
destination: 'qa.html#casos-prueba',
sectionId: 'casos-prueba'
};
// Fragmento del test parametrizado compartido.
await searchInput.sendKeys(caso.query);
const result = await driver.wait(
until.elementLocated(
By.css(`.portfolio-search-result[href="${caso.destination}"]`)
),
10000
);
assert.equal(await result.isDisplayed(), true);
await result.click();
await driver.wait(until.urlContains(caso.destination), 10000);
assert.ok((await driver.getCurrentUrl()).endsWith(caso.destination));
const section = await driver.findElement(By.id(caso.sectionId));
assert.equal(await section.isDisplayed(), true);
const posicion = await driver.executeScript(`
const section = arguments[0];
const navbar = document.querySelector('.navbar');
return {
sectionTop: section.getBoundingClientRect().top,
navbarBottom: navbar ? navbar.getBoundingClientRect().bottom : 0
};
`, section);
assert.ok(posicion.sectionTop >= posicion.navbarBottom - 2);
Fragmento representativo del test parametrizado compartido; se muestran los datos de SEARCH-TC-040 y las comprobaciones de resultado, URL, sección y cabecera.
Verifica que la búsqueda de «trazabilidad» permita acceder a quality-management.html#trazabilidad, que la sección de destino sea visible y que no quede oculta detrás de la cabecera fija.
const caso = {
id: 'SEARCH-TC-041',
query: 'trazabilidad',
destination: 'quality-management.html#trazabilidad',
sectionId: 'trazabilidad'
};
// Fragmento del test parametrizado compartido.
await searchInput.sendKeys(caso.query);
const result = await driver.wait(
until.elementLocated(
By.css(`.portfolio-search-result[href="${caso.destination}"]`)
),
10000
);
assert.equal(await result.isDisplayed(), true);
await result.click();
await driver.wait(until.urlContains(caso.destination), 10000);
assert.ok((await driver.getCurrentUrl()).endsWith(caso.destination));
const section = await driver.findElement(By.id(caso.sectionId));
assert.equal(await section.isDisplayed(), true);
const posicion = await driver.executeScript(`
const section = arguments[0];
const navbar = document.querySelector('.navbar');
return {
sectionTop: section.getBoundingClientRect().top,
navbarBottom: navbar ? navbar.getBoundingClientRect().bottom : 0
};
`, section);
assert.ok(posicion.sectionTop >= posicion.navbarBottom - 2);
Fragmento representativo del test parametrizado compartido; se muestran los datos de SEARCH-TC-041 y las comprobaciones de resultado, URL, sección y cabecera.
Verifica que la búsqueda de «persistencia» permita acceder a backend.html#persistencia, que la sección de destino sea visible y que no quede oculta detrás de la cabecera fija.
const caso = {
id: 'SEARCH-TC-042',
query: 'persistencia',
destination: 'backend.html#persistencia',
sectionId: 'persistencia'
};
// Fragmento del test parametrizado compartido.
await searchInput.sendKeys(caso.query);
const result = await driver.wait(
until.elementLocated(
By.css(`.portfolio-search-result[href="${caso.destination}"]`)
),
10000
);
assert.equal(await result.isDisplayed(), true);
await result.click();
await driver.wait(until.urlContains(caso.destination), 10000);
assert.ok((await driver.getCurrentUrl()).endsWith(caso.destination));
const section = await driver.findElement(By.id(caso.sectionId));
assert.equal(await section.isDisplayed(), true);
const posicion = await driver.executeScript(`
const section = arguments[0];
const navbar = document.querySelector('.navbar');
return {
sectionTop: section.getBoundingClientRect().top,
navbarBottom: navbar ? navbar.getBoundingClientRect().bottom : 0
};
`, section);
assert.ok(posicion.sectionTop >= posicion.navbarBottom - 2);
Fragmento representativo del test parametrizado compartido; se muestran los datos de SEARCH-TC-042 y las comprobaciones de resultado, URL, sección y cabecera.
Verifica que la búsqueda de «internacionalización» permita acceder a frontend.html#internacionalizacion, que la sección de destino sea visible y que no quede oculta detrás de la cabecera fija.
const caso = {
id: 'SEARCH-TC-043',
query: 'internacionalización',
destination: 'frontend.html#internacionalizacion',
sectionId: 'internacionalizacion'
};
// Fragmento del test parametrizado compartido.
await searchInput.sendKeys(caso.query);
const result = await driver.wait(
until.elementLocated(
By.css(`.portfolio-search-result[href="${caso.destination}"]`)
),
10000
);
assert.equal(await result.isDisplayed(), true);
await result.click();
await driver.wait(until.urlContains(caso.destination), 10000);
assert.ok((await driver.getCurrentUrl()).endsWith(caso.destination));
const section = await driver.findElement(By.id(caso.sectionId));
assert.equal(await section.isDisplayed(), true);
const posicion = await driver.executeScript(`
const section = arguments[0];
const navbar = document.querySelector('.navbar');
return {
sectionTop: section.getBoundingClientRect().top,
navbarBottom: navbar ? navbar.getBoundingClientRect().bottom : 0
};
`, section);
assert.ok(posicion.sectionTop >= posicion.navbarBottom - 2);
Fragmento representativo del test parametrizado compartido; se muestran los datos de SEARCH-TC-043 y las comprobaciones de resultado, URL, sección y cabecera.
Verifica que la búsqueda de «regresión» permita acceder a qa.html#regresion-bugs, que la sección de destino sea visible y que no quede oculta detrás de la cabecera fija.
const caso = {
id: 'SEARCH-TC-044',
query: 'regresión',
destination: 'qa.html#regresion-bugs',
sectionId: 'regresion-bugs'
};
// Fragmento del test parametrizado compartido.
await searchInput.sendKeys(caso.query);
const result = await driver.wait(
until.elementLocated(
By.css(`.portfolio-search-result[href="${caso.destination}"]`)
),
10000
);
assert.equal(await result.isDisplayed(), true);
await result.click();
await driver.wait(until.urlContains(caso.destination), 10000);
assert.ok((await driver.getCurrentUrl()).endsWith(caso.destination));
const section = await driver.findElement(By.id(caso.sectionId));
assert.equal(await section.isDisplayed(), true);
const posicion = await driver.executeScript(`
const section = arguments[0];
const navbar = document.querySelector('.navbar');
return {
sectionTop: section.getBoundingClientRect().top,
navbarBottom: navbar ? navbar.getBoundingClientRect().bottom : 0
};
`, section);
assert.ok(posicion.sectionTop >= posicion.navbarBottom - 2);
Fragmento representativo del test parametrizado compartido; se muestran los datos de SEARCH-TC-044 y las comprobaciones de resultado, URL, sección y cabecera.
Registro de defectos detectados, correcciones y pruebas de regresión relacionadas.
Estado histórico: Cerrado. Verificación documental: Regresión automatizada satisfactoria · 3/3 PASS
Severidad: Media
Prioridad: Alta
Requisito: FR-008
Caso relacionado:
TC-012
Prueba automatizada relacionada:
BUG-001 ·
phone-validation-boundaries.test.js
· 3/3 PASS · Chrome, Firefox y Edge
Windows · Google Chrome · Portfolio v1.0 (dato histórico)
Windows · Google Chrome, Firefox y Edge · Regresión del 30/10/2026
El usuario se encuentra en la pantalla de registro.
abc
en el campo teléfono.
El sistema rechaza el teléfono inválido y muestra un mensaje de validación.
El defecto histórico documentó que el formulario permitía continuar con un teléfono que contenía caracteres alfabéticos, utilizando abc como dato de reproducción. La regresión Selenium del 30/10/2026 verificó el rechazo del valor 12345abc, la presentación del mensaje de validación y el bloqueo del acceso al portfolio en Chrome, Firefox y Edge, con 3/3 PASS. Esta prueba confirma el comportamiento actual, pero no reconstruye por sí sola la corrección histórica del defecto.
Evidencia automatizada complementaria: el escenario BUG-001 del archivo phone-validation-boundaries.test.js verificó el rechazo del teléfono 12345abc en Chrome, Firefox y Edge, con 3/3 PASS. El código Selenium y las aserciones correspondientes pueden consultarse en la sección 02, Automatización con Selenium. Esta prueba forma parte de la suite telefónica completa de 33/33 PASS.
BUG-001 permanece cerrado según el registro histórico. La regresión Selenium del 30/10/2026 confirmó el rechazo de caracteres alfabéticos en el teléfono y el bloqueo del acceso al portfolio, con 3/3 PASS en Chrome, Firefox y Edge. La evidencia acredita el comportamiento actual, sin reconstruir la corrección histórica del defecto.
Estado histórico: Cerrado. Verificación documental: Regresión automatizada satisfactoria · 3/3 PASS
Severidad histórica: Alta
Prioridad histórica: Alta
Requisito principal: FR-012 · FR-002 complementario
Prueba automatizada relacionada:
BUG-002 ·
invalid-first-name-numeric.test.js
· 3/3 PASS · Chrome, Firefox y Edge
"Windows · Google Chrome · Portfolio v0.1 (dato histórico)",
"Windows · Google Chrome, Firefox y Edge · Regresión del 30/10/2026",
El usuario se encuentra en la pantalla de acceso.
sdfh567 o klsjdbg654#&%&
como nombre.
El sistema debe rechazar un nombre que no cumpla las reglas de validación.
El registro histórico indica que el formulario permitía continuar con un nombre inválido. La regresión Selenium del 30/10/2026 confirmó que el valor sdfh567 es rechazado, se muestra el mensaje de validación y se impide el acceso al portfolio en Chrome, Firefox y Edge (3/3 PASS). Esta verificación confirma el comportamiento actual, pero no reconstruye la corrección histórica.
Esta evidencia respalda el comportamiento actual del formulario, pero no acredita por sí sola la reproducción del defecto original ni su corrección histórica.
Como evidencia complementaria, la prueba Selenium invalid-first-name-numeric.test.js verificó el rechazo del nombre sdfh567 en Chrome, Firefox y Edge el 30/10/2026, con 3/3 PASS. El código de la prueba puede consultarse en la sección 02.
Captura compartida con BUG-003 que muestra el rechazo de un nombre con caracteres numéricos. El campo aparece resaltado en rojo y presenta el mensaje de validación correspondiente.
Ver evidencia de BUG-002 · Validación del nombre →Estado histórico: Cerrado. Verificación documental: Regresión automatizada satisfactoria · 3/3 PASS
Severidad histórica: Alta
Prioridad histórica: Alta
Requisito principal: FR-012 · FR-002 complementario
Prueba automatizada relacionada:
BUG-003 ·
invalid-last-name-numeric.test.js
· 3/3 PASS · Chrome, Firefox y Edge
Windows · Google Chrome · Portfolio v0.1 (dato histórico)
"Windows · Google Chrome, Firefox y Edge · Regresión del 30/10/2026"
El usuario se encuentra en la pantalla de acceso.
Flor567 o ddbhad&"%#3516
como apellido.
El sistema debe rechazar un apellido que no cumpla las reglas de validación.
El registro histórico indica que el formulario permitía continuar con un apellido inválido. La regresión Selenium del 30/10/2026 confirmó que el valor Flor567 es rechazado, se muestra el mensaje de validación y se impide el acceso al portfolio en Chrome, Firefox y Edge (3/3 PASS). Esta verificación confirma el comportamiento actual, pero no reconstruye la corrección histórica.
Captura compartida con BUG-002 que muestra el rechazo de un apellido que contiene caracteres numéricos. El campo aparece resaltado en rojo y presenta el mensaje de validación correspondiente.
Esta evidencia respalda el comportamiento actual del formulario, pero no acredita por sí sola la reproducción del defecto original ni su corrección histórica.
Como evidencia complementaria, la prueba Selenium invalid-last-name-numeric.test.js verificó el rechazo del apellido Flor567 en Chrome, Firefox y Edge el 30/10/2026, con 3/3 PASS. El código de la prueba puede consultarse en la sección 02.
Ver evidencia de BUG-003 · Validación del apellido →Estado histórico: Cerrado.
Clasificación actual: Observación de requisitos · Defecto funcional no confirmado.
Severidad histórica: Alta · Clasificación inicial, no vigente como defecto funcional.
Prioridad histórica: Alta · Clasificación inicial, no vigente como defecto funcional.
Requisito histórico: FR-003
Caso relacionado: TC-004
Prueba automatizada relacionada:
invalid-email-double-at.test.js
Verificación documental: Regresión automatizada satisfactoria · 15/15 PASS
La observación surgió durante la evaluación del formulario, al interpretar que validar un email también implicaba comprobar la existencia de la dirección o del buzón de correo. La revisión de FR-003 confirmó que el requisito especificaba únicamente la validación del formato. La diferencia se originó en la interpretación de su alcance, no en una ambigüedad demostrada de su redacción.
Windows · Google Chrome · Portfolio v0.1 (dato histórico)
Windows · Google Chrome, Firefox y Edge · Regresión del 30/10/2026
El usuario se encuentra en la pantalla de acceso.
sfghfh@sfhfhg.adhfgh
como email.
El sistema debe validar que la dirección de correo electrónico cumpla las reglas de formato definidas y rechazar las entradas que no las cumplan. Esta validación sintáctica no implica comprobar la existencia del dominio o del buzón de correo.
La evidencia histórica muestra una dirección de correo con estructura sintáctica plausible. No demuestra que el portfolio permitiera acceder utilizando un formato inválido. No se confirmó un defecto funcional de validación de email. El registro se conserva como observación de requisitos.
La verificación posterior documentó que la interfaz identifica como inválido el correo "gabriel@@gmail.com", que contiene dos símbolos @. El campo se resalta en rojo y presenta el mensaje "Ingresá un email válido". Esta captura acredita el comportamiento visual observado. La verificación automatizada del rechazo del acceso se documenta por separado en la regresión Selenium de FR-003.
Cinco formatos de correo electrónico inválidos verificados en Chrome, Firefox y Edge.
gabriel@@gmail.comgabriel@gabriel@prueba@gmail.comgabriel@gmail.Resultado de regresión: 15/15 PASS
La regresión verificó el rechazo de los formatos evaluados, los mensajes de validación y el bloqueo del acceso al portfolio. Estos resultados respaldan el cumplimiento de FR-003 para los escenarios ejecutados.
La captura documenta la validación visual posterior del correo "gabriel@@gmail.com". Se conserva el nombre original del archivo por trazabilidad histórica; la palabra "corregido" no demuestra que existiera un defecto funcional previo.
Ver evidencia de BUG-004 · Validación del email →BUG-004 se conserva como referencia histórica y se reclasifica como una observación de requisitos. FR-003 y TC-004 mantienen su trazabilidad original. No se confirmó un defecto funcional ni corresponde atribuir una corrección a esta observación. Las 15 ejecuciones Selenium satisfactorias constituyen evidencia complementaria del comportamiento verificado.
Estado histórico: Cerrado. Verificación documental: Regresión automatizada satisfactoria · 3/3 PASS
Severidad histórica: Baja
Prioridad histórica: Alta
Requisito histórico: FR-008
Caso relacionado:
TC-018
Prueba automatizada relacionada:
BUG-005 ·
phone-validation-boundaries.test.js
· 3/3 PASS · Chrome, Firefox y Edge
Windows · Google Chrome · Portfolio v0.1 (dato histórico)
Windows · Google Chrome, Firefox y Edge · Regresión del 30/10/2026
El usuario se encuentra en la pantalla de acceso.
Se verificaron valores inferiores al mínimo, valores dentro del rango permitido y valores superiores al máximo.
123456
· 6 dígitos · Rechazado · PASS
1234567
· 7 dígitos · Aceptado · PASS
123456789012345
· 15 dígitos · Aceptado · PASS
1234567890123456
· 16 dígitos · Rechazado · PASS
Suite telefónica completa: 33/33 PASS. Escenario específico BUG-005: 3/3 PASS. Navegadores: Chrome, Firefox y Edge.
El sistema debe rechazar un teléfono que supere la longitud máxima permitida.
El registro histórico indica que el formulario permitía continuar con un teléfono que superaba la longitud máxima admitida. La regresión Selenium del 30/10/2026 confirmó que el valor 1234567890123456, de 16 dígitos, es rechazado y que se impide el acceso al portfolio en Chrome, Firefox y Edge (3/3 PASS). La suite completa de validación telefónica obtuvo 33/33 PASS, incluyendo escenarios de límites inferiores, valores válidos y límites superiores. Esta verificación confirma el comportamiento actual, pero no reconstruye la corrección histórica.
El caso TC-018 registra un resultado PASS para el rechazo de teléfonos que exceden los 15 dígitos permitidos. Este resultado verifica el comportamiento documentado, pero no confirma por sí solo la trazabilidad histórica de la corrección de BUG-005.
Captura de verificación posterior con un teléfono de 25 dígitos, superior al máximo permitido de 15. La interfaz identifica el valor como inválido y muestra el mensaje de validación correspondiente. Esta evidencia respalda el comportamiento esperado de TC-018, sin acreditar por sí sola el defecto original ni la corrección histórica de BUG-005.
Evidencia automatizada complementaria: phone-validation-boundaries.test.js. El código de la prueba y sus resultados están documentados en la sección 02, Automatización con Selenium.
Ver evidencia de BUG-005 · TC-018 →Repositorio documental del proceso de QA: planificación, casos de prueba, resultados de ejecución y registro de defectos, con acceso a los archivos y evidencias originales del proyecto.
Especificaciones de pruebas funcionales y de API, con requisitos asociados, precondiciones, datos de entrada, pasos de ejecución y resultados esperados.
Consultar casos de prueba →Planificación del proceso de QA, con alcance, estrategia de pruebas, entorno de ejecución, criterios de entrada y salida, y entregables definidos para el proyecto.
Consultar plan de pruebas →Historial documentado de ejecuciones funcionales, pruebas de integración, validaciones de API y regresiones automatizadas, con resultados registrados y seguimiento de las verificaciones realizadas.
TC-019 y TC-020 · Regresión multinavegador: 12/12 PASS en Chrome, Firefox y Edge.
Internacionalización CV y QA · 27/27 PASS en Chrome, Firefox y Edge.
Gestión de Calidad · Internacionalización ES/EN/PT: 66/66 PASS en Chrome, Firefox y Edge.
Formulario de acceso · Internacionalización ES/EN/PT: 15/15 PASS en Chrome, Firefox y Edge.
Laboratorio Frontend · Internacionalización ES/EN/PT: 15/15 PASS en Chrome, Firefox y Edge.
Laboratorio Backend · Internacionalización ES/EN/PT: 15/15 PASS en Chrome, Firefox y Edge.
Consultar resultados →Registro documental de incidencias detectadas durante las pruebas, con descripción de los defectos, impacto, seguimiento de correcciones y evidencias disponibles para su consulta.
Consultar registro de defectos →