test(e2e): afinar el diagnóstico del mapa gris — no es DefMapLayers
/zones y /fires usan el mismo componente con las mismas props y en staging uno pinta 18 teselas y el otro cero, con el mapa inicializado y con vista en ambos. Hay que mirar cómo FiresMap monta los hijos del MapContainer, no el componente de capas. Y el detalle de fuego pide `satellite`, una capa de Google que ni existe hasta que se resuelve la clave. Además el script de QA deja de usar selectores por id: TestUtils.testId() devuelve el id SOLO en desarrollo, así que en staging y producción no existen — cualquier prueba de navegador escrita con ids pasa en local y falla en staging.
This commit is contained in:
parent
1153556f47
commit
b0ca838788
2 changed files with 82 additions and 37 deletions
|
|
@ -21,6 +21,9 @@ import { chromium } from '@playwright/test';
|
|||
|
||||
const BASE = (process.env.QA_URL || 'https://testfuegos.comunes.org').replace(/\/$/, '');
|
||||
const WRITE = process.argv.includes('--write');
|
||||
// Id hexadecimal de un activefire para probar la página de detalle. Se saca en
|
||||
// solo lectura del propio staging; sin él esa parte se salta.
|
||||
const FIRE = process.env.QA_FIRE || '';
|
||||
const SHOTS = path.resolve(import.meta.dirname, 'qa-shots');
|
||||
|
||||
// Comparación de host exacta, no un `endsWith`: testfuegos.comunes.org también
|
||||
|
|
@ -31,6 +34,19 @@ if (PROD_HOSTS.includes(new URL(BASE).hostname)) {
|
|||
process.exit(1);
|
||||
}
|
||||
|
||||
// ⚠️ NADA de selectores por id: `imports/ui/components/Utils/TestUtils.js`
|
||||
// devuelve el id solo en desarrollo, así que en staging y producción los
|
||||
// elementos NO lo llevan. Aquí se va por href, rol y texto, que es lo que hay
|
||||
// en los tres sitios.
|
||||
const NAV = {
|
||||
zonas: 'nav a[href="/zones"]',
|
||||
fuegos: 'nav a[href="/fires"]',
|
||||
perfil: 'nav a[href="/profile"]',
|
||||
entrar: 'nav a[href="/login"]',
|
||||
salir: 'nav a[href="/logout"]',
|
||||
misZonas: 'nav a[href="/subscriptions"]'
|
||||
};
|
||||
|
||||
const results = [];
|
||||
let page;
|
||||
|
||||
|
|
@ -51,6 +67,17 @@ const check = async (name, fn) => {
|
|||
|
||||
const assert = (cond, msg) => { if (!cond) throw new Error(msg); };
|
||||
|
||||
// La página es una SPA: tras `domcontentloaded` el body está casi vacío hasta
|
||||
// que React pinta. Sin esperar a esto, medio checklist daba falsos fallos.
|
||||
const rendered = async (minChars = 400) => {
|
||||
await page.waitForFunction(
|
||||
n => document.body && document.body.innerText.trim().length > n,
|
||||
minChars,
|
||||
{ timeout: 45000 }
|
||||
).catch(() => { throw new Error('la página no llegó a pintarse'); });
|
||||
return (await page.locator('body').innerText()).length;
|
||||
};
|
||||
|
||||
const visible = async (selector, msg) => {
|
||||
const el = page.locator(selector).first();
|
||||
await el.waitFor({ state: 'visible', timeout: 30000 }).catch(() => {
|
||||
|
|
@ -84,6 +111,7 @@ const main = async () => {
|
|||
const res = await page.goto(BASE, { waitUntil: 'domcontentloaded' });
|
||||
assert(res.ok(), `HTTP ${res.status()}`);
|
||||
await visible('nav');
|
||||
await rendered();
|
||||
return `HTTP ${res.status()}`;
|
||||
});
|
||||
await check('la portada monta sus mapas', mapReady);
|
||||
|
|
@ -104,10 +132,13 @@ const main = async () => {
|
|||
return m ? `${m[1]} fuegos` : 'sin contador visible';
|
||||
});
|
||||
await check('conmuta la capa base del mapa', async () => {
|
||||
await page.locator('.leaflet-control-layers-toggle').first().hover();
|
||||
// Hover sobre el contenedor, no sobre el botón: al desplegarse, la lista se
|
||||
// pone encima del botón y le roba el puntero.
|
||||
await page.locator('.leaflet-control-layers').first().hover();
|
||||
await page.waitForTimeout(500);
|
||||
const options = await page.locator('.leaflet-control-layers-base label').count();
|
||||
assert(options >= 2, `solo ${options} capa(s) base`);
|
||||
await page.locator('.leaflet-control-layers-base input').nth(1).check();
|
||||
await page.locator('.leaflet-control-layers-base input').nth(1).check({ force: true });
|
||||
await page.waitForTimeout(2000);
|
||||
return `${options} capas base`;
|
||||
});
|
||||
|
|
@ -123,25 +154,28 @@ const main = async () => {
|
|||
});
|
||||
|
||||
console.log('\n— Detalle de fuego y comentarios');
|
||||
await check('hay un fuego al que entrar', async () => {
|
||||
await page.goto(`${BASE}/fires`, { waitUntil: 'domcontentloaded' });
|
||||
await mapReady();
|
||||
const markers = await page.locator('.leaflet-marker-icon, .leaflet-interactive').count();
|
||||
assert(markers > 0, 'el mapa de fuegos no muestra ninguna marca');
|
||||
return `${markers} marcas`;
|
||||
});
|
||||
await check('el detalle de un fuego abre con su mapa', async () => {
|
||||
await page.locator('.leaflet-interactive').first().click();
|
||||
await page.waitForURL(/\/fire\//, { timeout: 30000 });
|
||||
await visible('h4.page-header');
|
||||
await mapReady();
|
||||
return page.url().replace(BASE, '');
|
||||
});
|
||||
await check('el detalle ofrece comentarios', async () => {
|
||||
const text = await page.locator('body').innerText();
|
||||
assert(/Comentarios/i.test(text), 'no aparece la sección de comentarios');
|
||||
return 'sección presente';
|
||||
});
|
||||
// Por id, no clicando una marca del mapa: mientras /fires siga en gris, ese
|
||||
// camino no existe, y el detalle del fuego se puede probar igualmente.
|
||||
if (!FIRE) {
|
||||
console.log(' (saltado) sin QA_FIRE=<id hex de un activefire> no se puede abrir un detalle');
|
||||
} else {
|
||||
await check('el detalle de un fuego abre con su mapa', async () => {
|
||||
await page.goto(`${BASE}/fire/active/${FIRE}`, { waitUntil: 'domcontentloaded' });
|
||||
await visible('h4.page-header');
|
||||
await rendered();
|
||||
await mapReady();
|
||||
return page.url().replace(BASE, '');
|
||||
});
|
||||
await check('el detalle canonicaliza la URL al archivo', async () => {
|
||||
assert(/\/fire\/archive\/[0-9a-f]{24}$/.test(page.url()), `URL inesperada: ${page.url()}`);
|
||||
return page.url().replace(BASE, '');
|
||||
});
|
||||
await check('el detalle ofrece comentarios', async () => {
|
||||
const text = await page.locator('body').innerText();
|
||||
assert(/Comentarios/i.test(text), 'no aparece la sección de comentarios');
|
||||
return 'sección presente';
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n— Idioma y cookies');
|
||||
await check('el banner de cookies aparece y se acepta', async () => {
|
||||
|
|
@ -155,18 +189,21 @@ const main = async () => {
|
|||
const enContext = await browser.newContext({ locale: 'en-US' });
|
||||
const enPage = await enContext.newPage();
|
||||
await enPage.goto(BASE, { waitUntil: 'domcontentloaded' });
|
||||
await enPage.waitForTimeout(3000);
|
||||
const label = await enPage.locator('#moniZones').innerText().catch(() => '');
|
||||
await enPage.waitForFunction((sel) => {
|
||||
const el = document.querySelector(sel);
|
||||
return el && el.textContent.trim().length > 0;
|
||||
}, NAV.zonas, { timeout: 45000 }).catch(() => {});
|
||||
const label = await enPage.locator(NAV.zonas).innerText().catch(() => '');
|
||||
await enContext.close();
|
||||
assert(/Monitored/i.test(label), `la navegación sigue en español: "${label}"`);
|
||||
return `#moniZones = "${label}"`;
|
||||
return `enlace de zonas = "${label}"`;
|
||||
});
|
||||
await check('el mismo servidor sirve español a un navegador en español', async () => {
|
||||
await page.goto(BASE, { waitUntil: 'domcontentloaded' });
|
||||
await page.waitForTimeout(2000);
|
||||
const label = await page.locator('#moniZones').innerText();
|
||||
await rendered();
|
||||
const label = await page.locator(NAV.zonas).innerText();
|
||||
assert(/Zonas/i.test(label), `esperaba español, salió "${label}"`);
|
||||
return `#moniZones = "${label}"`;
|
||||
return `enlace de zonas = "${label}"`;
|
||||
});
|
||||
|
||||
console.log('\n— Páginas estáticas');
|
||||
|
|
@ -175,9 +212,8 @@ const main = async () => {
|
|||
await check(`${p} responde`, async () => {
|
||||
const res = await page.goto(BASE + p, { waitUntil: 'domcontentloaded' });
|
||||
assert(res.ok(), `HTTP ${res.status()}`);
|
||||
const text = await page.locator('body').innerText();
|
||||
assert(text.length > 200, 'la página viene casi vacía');
|
||||
return `${text.length} caracteres`;
|
||||
const chars = await rendered(600);
|
||||
return `${chars} caracteres`;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -190,8 +226,8 @@ const main = async () => {
|
|||
await page.fill('input[name=lastName]', 'Fase13');
|
||||
await page.fill('input[name=emailAddress]', email);
|
||||
await page.fill('input[name=password]', 'password-qa-13');
|
||||
await page.check('#tos');
|
||||
await page.click('#signUpSubmit');
|
||||
await page.check('input[name=tos]');
|
||||
await page.getByRole('button', { name: 'Registrarse', exact: true }).click();
|
||||
await page.waitForURL('**/subscriptions', { timeout: 60000 });
|
||||
return email;
|
||||
});
|
||||
|
|
@ -230,8 +266,8 @@ const main = async () => {
|
|||
return 'borrada';
|
||||
});
|
||||
await check('cierre de sesión', async () => {
|
||||
await page.click('#logout');
|
||||
await visible('#login');
|
||||
await page.locator(NAV.salir).click();
|
||||
await visible(NAV.entrar, 'no vuelve a aparecer el enlace de iniciar sesión');
|
||||
return 'sesión cerrada';
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,10 +22,19 @@ import { waitForMap } from '../support/app.js';
|
|||
// funcionaban— se quedan también sin teselas, porque entonces el control
|
||||
// aparece después de que el mapa ya esté montado. Probado en staging,
|
||||
// revertido: no basta con mover el momento del montaje.
|
||||
// 5. Y el culpable NO es DefMapLayers: /zones (SubscriptionsMap) y /fires
|
||||
// (FiresMap) usan el MISMO componente con las MISMAS props
|
||||
// (`<DefMapLayers gray />`), y en staging /zones pinta 18 teselas con su
|
||||
// capa en el pane mientras /fires se queda con CERO capas. En los dos casos
|
||||
// el mapa está inicializado y con vista (el `.leaflet-proxy` está ahí y el
|
||||
// pane tiene su transform): lo que falta en /fires es que se le añada la
|
||||
// capa base.
|
||||
//
|
||||
// El arreglo bueno pasa por que el conjunto de capas base esté decidido ANTES de
|
||||
// montar el mapa (resolver la clave más arriba, o que GoogleMutantLayer sepa no
|
||||
// hacer nada sin clave).
|
||||
// O sea que hay que mirar cómo FiresMap monta y re-renderiza los hijos del
|
||||
// MapContainer (tiene shouldComponentUpdate y un manejo de viewport con
|
||||
// debounce), no DefMapLayers. La página de detalle de fuego es un caso aparte y
|
||||
// además pide `satellite`, que es una capa de Google y por tanto ni siquiera
|
||||
// existe hasta que la clave se resuelve.
|
||||
//
|
||||
// Quedan como test.fixme para que salgan como pendientes en cada ejecución en
|
||||
// vez de desaparecer en una nota. Quitar el .fixme al arreglarlo.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue