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
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