test(e2e): browser suite for the flows the server tests cannot see
Playwright under e2e/, its own npm project so Meteor never sees the dependency (.meteorignore keeps the directory out of the bundle — a config file at the app root is otherwise eagerly loaded into the server and crashes it). Covers what the phase asked for: sign up / log in, creating a zone from the map and watching the union get painted on /subscriptions and /zones, removing it again, commenting on a fire, and switching language. The zone spec is the flow that froze staging on 2026-07-30 — it passes only if the server is still answering DDP while the union is recomputed. docker-compose.e2e.yml is a throwaway stack (Mongo on tmpfs, no named volume) so the destructive seeds cannot be pointed at anything real by accident; playwright.config.js refuses to start against the staging or production domains. i18n.spec.js leaves a test.fixme on a real bug found while writing it: the language chosen in the profile does not survive a page reload.
This commit is contained in:
parent
3b04ebceb3
commit
a3f2929ff7
14 changed files with 708 additions and 0 deletions
3
e2e/.gitignore
vendored
Normal file
3
e2e/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
node_modules/
|
||||
playwright-report/
|
||||
test-results/
|
||||
60
e2e/README.md
Normal file
60
e2e/README.md
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
# Suite e2e (Playwright)
|
||||
|
||||
Cubre en navegador los flujos que la suite de servidor no puede ver: registro y
|
||||
acceso, alta y baja de una zona **desde el mapa**, que la unión de zonas se pinte
|
||||
en `/subscriptions` y en `/zones`, comentario en el detalle de un fuego, y cambio
|
||||
de idioma.
|
||||
|
||||
El motivo de que exista: el 2026-07-30 suscribirse a una zona congeló staging
|
||||
(la unión bloqueaba el event loop y DDP dejaba de responder). Ningún test de
|
||||
servidor habría visto eso — el navegador sí.
|
||||
|
||||
## ⚠️ Contra qué se puede correr
|
||||
|
||||
Solo contra un stack **desechable**: `docker-compose.e2e.yml` o un servidor de
|
||||
desarrollo local. La siembra (`e2e/seed.js`) **borra colecciones**, y los tests
|
||||
crean usuarios y zonas. `playwright.config.js` se niega a arrancar si
|
||||
`E2E_BASE_URL` apunta a `testfuegos.comunes.org` o a los dominios de producción.
|
||||
|
||||
## Uso
|
||||
|
||||
Stack efímero completo (lo mismo que hace el CI):
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.e2e.yml up -d --build
|
||||
npm --prefix e2e ci
|
||||
npx --prefix e2e playwright install --with-deps chromium
|
||||
E2E_BASE_URL=http://localhost:3300 npm --prefix e2e test
|
||||
docker compose -f docker-compose.e2e.yml down -v
|
||||
```
|
||||
|
||||
Contra un servidor de desarrollo (ciclo rápido mientras se escriben tests):
|
||||
|
||||
```bash
|
||||
docker run -d --name tcef-e2e-mongo -p 27021:27017 --tmpfs /data/db mongo:7 --replSet rs0 --bind_ip_all
|
||||
docker exec tcef-e2e-mongo mongosh --quiet --eval 'rs.initiate({_id:"rs0",members:[{_id:0,host:"localhost:27017"}]})'
|
||||
docker cp e2e/seed.js tcef-e2e-mongo:/tmp/seed.js && docker exec tcef-e2e-mongo mongosh --quiet fuegos /tmp/seed.js
|
||||
|
||||
export PATH="$HOME/.meteor:$PATH"
|
||||
MONGO_URL='mongodb://localhost:27021/fuegos?replicaSet=rs0&directConnection=true' \
|
||||
MONGO_OPLOG_URL='mongodb://localhost:27021/local?replicaSet=rs0&directConnection=true' \
|
||||
ROOT_URL=http://localhost:3100 meteor --settings settings-ci.json --port 3100
|
||||
|
||||
npm --prefix e2e test # E2E_BASE_URL por defecto: http://localhost:3100
|
||||
```
|
||||
|
||||
Ver el informe del último fallo: `npm --prefix e2e run report`.
|
||||
|
||||
## Dependencias externas
|
||||
|
||||
El navegador carga las teselas de CartoDB y el script de Google Maps (el
|
||||
autocompletado de lugares). El runner necesita salida a internet; sin ella los
|
||||
mapas no llegan a pintarse y los tests de zona fallan. La clave de Google puede
|
||||
ir vacía: `Gkeys` ya no se queda esperándola.
|
||||
|
||||
## Lo que la suite documenta como roto
|
||||
|
||||
`i18n.spec.js` deja un `test.fixme` para un bug real: el idioma elegido en el
|
||||
perfil **no sobrevive a una recarga** (se guarda en la cuenta y se aplica al
|
||||
iniciar sesión, pero al recargar vuelve al idioma del navegador). Está ahí para
|
||||
que aparezca en cada ejecución en vez de perderse en una nota.
|
||||
76
e2e/package-lock.json
generated
Normal file
76
e2e/package-lock.json
generated
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
{
|
||||
"name": "tcef-e2e",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "tcef-e2e",
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.49.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
|
||||
"integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
|
||||
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.62.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.62.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
|
||||
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
e2e/package.json
Normal file
15
e2e/package.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "tcef-e2e",
|
||||
"private": true,
|
||||
"description": "Browser end-to-end suite for todos-contra-el-fuego-web. Kept out of the app's package.json on purpose: Meteor should never see these dependencies.",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test": "playwright test",
|
||||
"test:headed": "playwright test --headed",
|
||||
"report": "playwright show-report",
|
||||
"install-browsers": "playwright install --with-deps chromium"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.49.1"
|
||||
}
|
||||
}
|
||||
42
e2e/playwright.config.js
Normal file
42
e2e/playwright.config.js
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
// E2E_BASE_URL points at a THROWAWAY stack — the compose in docker-compose.e2e.yml
|
||||
// or a local dev server. Never at testfuegos.comunes.org or anything else with
|
||||
// data in it: these tests create users and subscriptions, and the seed step wipes
|
||||
// collections.
|
||||
const baseURL = process.env.E2E_BASE_URL || 'http://localhost:3100';
|
||||
|
||||
if (/testfuegos|fuegos\.comunes|fires\.comunes/.test(baseURL)) {
|
||||
throw new Error(`Refusing to run the e2e suite against ${baseURL}: it seeds and writes data.`);
|
||||
}
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
// Meteor's first page load compiles and ships a big bundle; the map then waits
|
||||
// for tiles and DDP subscriptions.
|
||||
timeout: 90 * 1000,
|
||||
expect: { timeout: 20 * 1000 },
|
||||
// Every spec creates its own user, but they share one Mongo and one union
|
||||
// recompute queue; running them in parallel makes failures much harder to read
|
||||
// for very little wall-clock on a runner that only has one job slot anyway.
|
||||
workers: 1,
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 1 : 0,
|
||||
reporter: process.env.CI
|
||||
? [['list'], ['html', { open: 'never', outputFolder: 'playwright-report' }]]
|
||||
: [['list']],
|
||||
use: {
|
||||
baseURL,
|
||||
// Both on first retry only: a trace per test would be gigabytes of artifacts
|
||||
// for a suite that is green most of the time.
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
actionTimeout: 20 * 1000,
|
||||
locale: 'es-ES'
|
||||
},
|
||||
projects: [
|
||||
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }
|
||||
]
|
||||
});
|
||||
61
e2e/seed.js
Normal file
61
e2e/seed.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Deterministic seed for the browser e2e suite (mongosh script).
|
||||
*
|
||||
* ⚠️ DESTRUCTIVE: it empties the collections it seeds. It is only ever meant for
|
||||
* the throwaway Mongo of docker-compose.e2e.yml (or a local scratch one). Never
|
||||
* point it at staging or production — that is what e2e/seed.sh guards against.
|
||||
*
|
||||
* mongosh --host localhost:27021 fuegos e2e/seed.js
|
||||
*
|
||||
* The subscriptions collection is deliberately NOT seeded: the whole point of
|
||||
* the zone spec is to create one through the UI and watch the union appear.
|
||||
*/
|
||||
|
||||
var FIRE_A = ObjectId('e2e0000000000000000000a1');
|
||||
var FIRE_B = ObjectId('e2e0000000000000000000b2');
|
||||
|
||||
// Madrid, so a zone created around the map's default view sees them.
|
||||
var LAT = 40.41;
|
||||
var LON = -3.70;
|
||||
var WHEN = ISODate('2026-07-30T05:30:00.000Z');
|
||||
var WHEN_B = ISODate('2026-07-30T06:30:00.000Z');
|
||||
var FIXED = ISODate('2026-07-30T00:00:00.000Z');
|
||||
|
||||
db.activefires.deleteMany({});
|
||||
db.activefires.insertMany([
|
||||
{
|
||||
_id: FIRE_A,
|
||||
ourid: { type: 'Point', coordinates: [LON, LAT] },
|
||||
lat: LAT, lon: LON, type: 'viirs', when: WHEN,
|
||||
scan: 0.5, track: 0.4, acq_date: '2026-07-30', acq_time: '05:30',
|
||||
satellite: 'N', confidence: 50, version: '1.0NRT', frp: 4.3,
|
||||
daynight: 'N', bright_ti4: 330.2, bright_ti5: 291,
|
||||
createdAt: FIXED, updatedAt: FIXED
|
||||
},
|
||||
{
|
||||
_id: FIRE_B,
|
||||
ourid: { type: 'Point', coordinates: [LON + 0.02, LAT + 0.02] },
|
||||
lat: LAT + 0.02, lon: LON + 0.02, type: 'viirs', when: WHEN_B,
|
||||
scan: 0.5, track: 0.4, acq_date: '2026-07-30', acq_time: '06:30',
|
||||
satellite: 'N', confidence: 60, version: '1.0NRT', frp: 6.1,
|
||||
daynight: 'N', bright_ti4: 331.0, bright_ti5: 292,
|
||||
createdAt: FIXED, updatedAt: FIXED
|
||||
}
|
||||
]);
|
||||
db.activefires.createIndex({ ourid: '2dsphere' });
|
||||
|
||||
db.activefiresunion.deleteMany({});
|
||||
db.activefiresunion.createIndex({ shape: '2dsphere' });
|
||||
|
||||
db.comments.deleteMany({});
|
||||
db.falsePositives.deleteMany({});
|
||||
db.industries.deleteMany({});
|
||||
|
||||
// Anything left over from a previous run of the suite: users it created, their
|
||||
// zones, and the stored union computed from them.
|
||||
db.subscriptions.deleteMany({});
|
||||
db.users.deleteMany({ 'emails.address': /^e2e\+/ });
|
||||
db.siteSettings.deleteMany({ name: /^subs-(public|private)-union/ });
|
||||
db.siteSettings.deleteMany({ name: 'subs-union-count' });
|
||||
|
||||
print('e2e seed OK: ' + db.activefires.countDocuments({}) + ' active fires, 0 subscriptions');
|
||||
54
e2e/support/app.js
Normal file
54
e2e/support/app.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Shared helpers. Everything here drives the UI the way a person would: the
|
||||
// point of this suite is the wiring between browser, DDP and Mongo, so reaching
|
||||
// into Meteor.call() from the page would defeat it.
|
||||
|
||||
// A fresh address per test: the seed removes every e2e+* user, and reusing one
|
||||
// across specs makes failures depend on the order they ran in.
|
||||
export const newEmail = () => `e2e+${Date.now()}${Math.floor(Math.random() * 1000)}@test.com`;
|
||||
|
||||
export const signUp = async (page, email = newEmail()) => {
|
||||
await page.goto('/signup');
|
||||
await page.fill('input[name=firstName]', 'E2E');
|
||||
await page.fill('input[name=lastName]', 'Tester');
|
||||
await page.fill('input[name=emailAddress]', email);
|
||||
await page.fill('input[name=password]', 'password');
|
||||
await page.check('#tos');
|
||||
await page.click('#signUpSubmit');
|
||||
// Signing up lands on the zones page.
|
||||
await page.waitForURL('**/subscriptions', { timeout: 30000 });
|
||||
return email;
|
||||
};
|
||||
|
||||
export const logIn = async (page, email, password = 'password') => {
|
||||
await page.goto('/login');
|
||||
await page.fill('input[name=emailAddress]', email);
|
||||
await page.fill('input[name=password]', password);
|
||||
await page.click('#loginSubmit');
|
||||
};
|
||||
|
||||
// The map only settles once leaflet has laid out its panes and the tiles for the
|
||||
// current viewport are in.
|
||||
export const waitForMap = async (page) => {
|
||||
await page.locator('.leaflet-container').first().waitFor({ state: 'visible', timeout: 60000 });
|
||||
await page.waitForTimeout(1500);
|
||||
};
|
||||
|
||||
// Creates a zone through the real flow: the button on the zones map, the
|
||||
// selection map, and the subscribe button. This is the flow that froze staging.
|
||||
export const addZoneThroughTheMap = async (page) => {
|
||||
await page.goto('/subscriptions');
|
||||
await waitForMap(page);
|
||||
await page.getByRole('button', { name: 'Añadir zona' }).click();
|
||||
await page.waitForURL('**/subscriptions/new', { timeout: 30000 });
|
||||
await waitForMap(page);
|
||||
|
||||
const map = page.locator('.leaflet-container').first();
|
||||
const box = await map.boundingBox();
|
||||
// Click slightly off-centre: the marker moves there, which also proves the
|
||||
// map is interactive and not a dead tile layer.
|
||||
await page.mouse.click(box.x + (box.width / 2) + 20, box.y + (box.height / 2) + 20);
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByRole('button', { name: /Suscribirme/ }).click();
|
||||
await page.waitForURL('**/subscriptions', { timeout: 60000 });
|
||||
};
|
||||
44
e2e/tests/auth.spec.js
Normal file
44
e2e/tests/auth.spec.js
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { signUp, logIn, newEmail } from '../support/app.js';
|
||||
|
||||
test.describe('registro y acceso', () => {
|
||||
test('a new account can sign up, log out and log back in', async ({ page }) => {
|
||||
const email = newEmail();
|
||||
await signUp(page, email);
|
||||
|
||||
// Signed in: the navbar swaps the login links for the profile and logout ones.
|
||||
await expect(page.locator('#logout')).toBeVisible();
|
||||
await expect(page.locator('#profile')).toContainText('E2E Tester');
|
||||
|
||||
await page.click('#logout');
|
||||
await expect(page.locator('#login')).toBeVisible();
|
||||
|
||||
await logIn(page, email);
|
||||
await expect(page.locator('#logout')).toBeVisible();
|
||||
});
|
||||
|
||||
test('signing up is refused without accepting the terms', async ({ page }) => {
|
||||
await page.goto('/signup');
|
||||
await page.fill('input[name=firstName]', 'E2E');
|
||||
await page.fill('input[name=lastName]', 'Tester');
|
||||
await page.fill('input[name=emailAddress]', newEmail());
|
||||
await page.fill('input[name=password]', 'password');
|
||||
|
||||
await expect(page.locator('#signUpSubmit')).toBeDisabled();
|
||||
});
|
||||
|
||||
test('a wrong password does not sign anybody in', async ({ page }) => {
|
||||
const email = newEmail();
|
||||
await signUp(page, email);
|
||||
await page.click('#logout');
|
||||
|
||||
await logIn(page, email, 'not-the-password');
|
||||
await expect(page.locator('#loginSubmit')).toBeVisible();
|
||||
await expect(page.locator('#logout')).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('the private zones page sends anonymous visitors to the login', async ({ page }) => {
|
||||
await page.goto('/subscriptions');
|
||||
await expect(page.locator('#loginSubmit')).toBeVisible();
|
||||
});
|
||||
});
|
||||
40
e2e/tests/fire-comment.spec.js
Normal file
40
e2e/tests/fire-comment.spec.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { signUp, waitForMap } from '../support/app.js';
|
||||
|
||||
// e2e/seed.js puts this one in activefires. Visiting an active fire archives it
|
||||
// and canonicalizes the URL to /fire/archive/<other id>, which is itself worth
|
||||
// pinning: it is the link every notification and every tweet points at.
|
||||
const SEEDED_FIRE = '/fire/active/e2e0000000000000000000a1';
|
||||
|
||||
test.describe('detalle de un fuego y comentarios', () => {
|
||||
test('a seeded fire opens, keeps its map and canonicalizes its URL', async ({ page }) => {
|
||||
await page.goto(SEEDED_FIRE);
|
||||
await expect(page.getByText(/Información adicional sobre fuego detectado/)).toBeVisible();
|
||||
await expect(page).toHaveURL(/\/fire\/archive\/[0-9a-f]{24}$/);
|
||||
await waitForMap(page);
|
||||
await expect(page.getByText('Coordenadas: 40.41, -3.7')).toBeVisible();
|
||||
});
|
||||
|
||||
test('a signed-in user can comment on a fire and sees it appear', async ({ page }) => {
|
||||
await signUp(page);
|
||||
await page.goto(SEEDED_FIRE);
|
||||
await expect(page.getByText('Comentarios')).toBeVisible();
|
||||
|
||||
const comment = `Ardió el pinar de la ladera norte ${Date.now()}`;
|
||||
await page.locator('textarea.create-comment').fill(comment);
|
||||
await page.getByRole('button', { name: 'Añadir comentario' }).click();
|
||||
|
||||
await expect(page.getByText(comment)).toBeVisible({ timeout: 30000 });
|
||||
|
||||
// And it is really stored, not just echoed into the DOM.
|
||||
await page.reload();
|
||||
await expect(page.getByText(comment)).toBeVisible({ timeout: 30000 });
|
||||
});
|
||||
|
||||
test('an anonymous visitor is asked to sign in instead of getting a comment box', async ({ page }) => {
|
||||
await page.goto(SEEDED_FIRE);
|
||||
await expect(page.getByRole('heading', { name: 'Comentarios' })).toBeVisible();
|
||||
await expect(page.getByText('Necesitas iniciar sesión para añadir comentarios')).toBeVisible();
|
||||
await expect(page.locator('textarea.create-comment')).toHaveCount(0);
|
||||
});
|
||||
});
|
||||
54
e2e/tests/i18n.spec.js
Normal file
54
e2e/tests/i18n.spec.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { signUp } from '../support/app.js';
|
||||
|
||||
// Through the navbar, not page.goto('/profile'): a cold load of that URL bounces
|
||||
// to /subscriptions (the Authenticated wrapper decides before Meteor has resumed
|
||||
// the login token). Worth its own fix, but it is not what this spec is about.
|
||||
const openProfile = async (page) => {
|
||||
await page.click('#profile');
|
||||
await page.waitForURL('**/profile', { timeout: 30000 });
|
||||
};
|
||||
|
||||
const chooseLanguage = async (page, name) => {
|
||||
await page.locator('.lang-selector').first().click();
|
||||
await page.getByRole('button', { name }).click();
|
||||
};
|
||||
|
||||
// The site is bilingual (es/en) and the language belongs to the account, not to
|
||||
// the browser session: notifications and emails go out in it too.
|
||||
test.describe('cambio de idioma', () => {
|
||||
test('the interface follows the language chosen in the profile', async ({ page }) => {
|
||||
await signUp(page);
|
||||
await expect(page.locator('#moniZones')).toHaveText('Zonas vigiladas');
|
||||
|
||||
await openProfile(page);
|
||||
await chooseLanguage(page, 'English');
|
||||
|
||||
await expect(page.locator('#moniZones')).toHaveText('Monitored areas', { timeout: 30000 });
|
||||
});
|
||||
|
||||
test('going back to Spanish works too', async ({ page }) => {
|
||||
await signUp(page);
|
||||
await openProfile(page);
|
||||
await chooseLanguage(page, 'English');
|
||||
await expect(page.locator('#moniZones')).toHaveText('Monitored areas', { timeout: 30000 });
|
||||
|
||||
await chooseLanguage(page, 'Español');
|
||||
await expect(page.locator('#moniZones')).toHaveText('Zonas vigiladas', { timeout: 30000 });
|
||||
});
|
||||
|
||||
// KNOWN BUG, kept as a failing-by-design test instead of being quietly dropped:
|
||||
// users.setLang does store the choice on the account, and Login.js applies it
|
||||
// when you log in — but a plain reload of an already-logged-in session falls
|
||||
// back to the browser's locale. Someone who picked English sees Spanish again
|
||||
// on the next page load.
|
||||
test.fixme('the chosen language survives a reload', async ({ page }) => {
|
||||
await signUp(page);
|
||||
await openProfile(page);
|
||||
await chooseLanguage(page, 'English');
|
||||
await expect(page.locator('#moniZones')).toHaveText('Monitored areas', { timeout: 30000 });
|
||||
|
||||
await page.reload();
|
||||
await expect(page.locator('#moniZones')).toHaveText('Monitored areas', { timeout: 30000 });
|
||||
});
|
||||
});
|
||||
54
e2e/tests/zone-union.spec.js
Normal file
54
e2e/tests/zone-union.spec.js
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
import { signUp, addZoneThroughTheMap, waitForMap } from '../support/app.js';
|
||||
|
||||
// This is the flow that froze staging on 2026-07-30: subscribing to a zone makes
|
||||
// the server recompute the union of every subscription, and while that ran on the
|
||||
// main thread DDP stopped answering and every browser hung. A green run here means
|
||||
// the page came back, the union reached the client, and the map drew it.
|
||||
test.describe('suscripción a una zona y unión de zonas', () => {
|
||||
test('creating a zone lists it and paints the union on both maps', async ({ page }) => {
|
||||
await signUp(page);
|
||||
await expect(page.getByText('No estás suscrito a fuegos en ninguna zona')).toBeVisible();
|
||||
|
||||
await addZoneThroughTheMap(page);
|
||||
|
||||
// Back on the zones page, the "you have no zones" warning is gone.
|
||||
await expect(page.getByText('No estás suscrito a fuegos en ninguna zona')).toHaveCount(0);
|
||||
|
||||
// The union is a GeoJSON layer: leaflet draws it as an SVG path in the
|
||||
// overlay pane. Nothing is drawn there until the server has recomputed and
|
||||
// published it, so this also pins that the recompute finishes at all.
|
||||
await waitForMap(page);
|
||||
await expect(page.locator('.leaflet-overlay-pane path').first()).toBeVisible({ timeout: 60000 });
|
||||
|
||||
await page.goto('/zones');
|
||||
await waitForMap(page);
|
||||
await expect(page.locator('.leaflet-overlay-pane path').first()).toBeVisible({ timeout: 60000 });
|
||||
});
|
||||
|
||||
test('the site keeps answering while the union is recomputed', async ({ page }) => {
|
||||
await signUp(page);
|
||||
await addZoneThroughTheMap(page);
|
||||
|
||||
// A DDP round trip right after the insert: if the union blocked the event
|
||||
// loop again, this navigation would hang instead of rendering.
|
||||
await page.goto('/fires');
|
||||
await waitForMap(page);
|
||||
await expect(page.locator('.leaflet-container').first()).toBeVisible();
|
||||
});
|
||||
|
||||
// Removing is the other half of the same server-side path: it schedules a full
|
||||
// recompute instead of an incremental merge.
|
||||
test('a zone can be removed again from the map', async ({ page }) => {
|
||||
await signUp(page);
|
||||
await addZoneThroughTheMap(page);
|
||||
await expect(page.getByText('En verde, áreas de las que recibirás alertas de fuegos')).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: /Editar/i }).click();
|
||||
await page.locator('.leaflet-marker-icon').first().click();
|
||||
await expect(page.getByText('¿Estás seguro/a?')).toBeVisible();
|
||||
await page.getByRole('button', { name: 'Sí', exact: true }).click();
|
||||
|
||||
await expect(page.getByText('No estás suscrito a fuegos en ninguna zona')).toBeVisible({ timeout: 60000 });
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue