feat(frontend): add mobile-first Material redesign (#17)
This commit is contained in:
1 parent
fd3fc0123a
commit
1a70bcd126
15 files changed
+1781
-721
No files matched your search
@@ -0,0 +1,197 @@
|
||||
import { expect, Page, test } from '@playwright/test';
|
||||
|
||||
async function mockUser(page: Page, authenticated: boolean): Promise<void> {
|
||||
await page.unroute('**/api/user');
|
||||
await page.route('**/api/user', async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: 'application/json',
|
||||
json: authenticated
|
||||
? { authenticated: true, username: 'Alice', email: 'alice@example.com' }
|
||||
: { authenticated: false, username: '', email: '' },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function mockEmptyApartmentList(page: Page): Promise<void> {
|
||||
await page.route('**/api/apartments', async (route) => {
|
||||
await route.fulfill({ contentType: 'application/json', json: [] });
|
||||
});
|
||||
}
|
||||
|
||||
async function expectNoHorizontalOverflow(page: Page): Promise<void> {
|
||||
const hasHorizontalOverflow = await page.evaluate(
|
||||
() => document.documentElement.scrollWidth > document.documentElement.clientWidth,
|
||||
);
|
||||
expect(hasHorizontalOverflow).toBe(false);
|
||||
}
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await mockUser(page, false);
|
||||
});
|
||||
|
||||
test('renders the sign-in experience without horizontal overflow', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
await expect(page.getByRole('heading', { name: 'LMNP Organizer' })).toBeVisible();
|
||||
await expect(page.getByRole('button', { name: 'Se connecter avec OIDC' })).toBeVisible();
|
||||
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test('starts the OIDC login flow from the sign-in screen', async ({ page }) => {
|
||||
await page.route('**/oauth2/authorization/oidc', async (route) => {
|
||||
await route.fulfill({ contentType: 'text/html', body: '<h1>OIDC provider</h1>' });
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Se connecter avec OIDC' }).click();
|
||||
|
||||
await expect(page).toHaveURL(/\/oauth2\/authorization\/oidc$/);
|
||||
});
|
||||
|
||||
test('persists a dark appearance preference from Settings', async ({ page }) => {
|
||||
await mockUser(page, true);
|
||||
await mockEmptyApartmentList(page);
|
||||
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Réglages' }).click();
|
||||
await page.getByRole('radio', { name: 'Sombre' }).check();
|
||||
|
||||
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
|
||||
expect(await page.evaluate(() => localStorage.getItem('lmnp-theme'))).toBe('dark');
|
||||
|
||||
await page.reload();
|
||||
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
|
||||
await page.getByRole('button', { name: 'Réglages' }).click();
|
||||
await expect(page.getByRole('radio', { name: 'Sombre' })).toBeChecked();
|
||||
});
|
||||
|
||||
test('adapts authenticated navigation to the viewport', async ({ page }, testInfo) => {
|
||||
await mockUser(page, true);
|
||||
await mockEmptyApartmentList(page);
|
||||
|
||||
await page.goto('/');
|
||||
|
||||
const navigation = page.getByTestId('primary-navigation');
|
||||
await expect(navigation).toBeVisible();
|
||||
|
||||
const navigationBox = await navigation.boundingBox();
|
||||
const viewport = page.viewportSize();
|
||||
expect(navigationBox).not.toBeNull();
|
||||
expect(viewport).not.toBeNull();
|
||||
if (testInfo.project.name === 'desktop') {
|
||||
expect(navigationBox?.x).toBe(0);
|
||||
expect(navigationBox?.height).toBe(viewport?.height);
|
||||
} else {
|
||||
expect(Math.round((navigationBox?.y ?? 0) + (navigationBox?.height ?? 0))).toBe(
|
||||
viewport?.height,
|
||||
);
|
||||
}
|
||||
|
||||
await navigation.getByRole('button', { name: 'Appartements' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Mes appartements' })).toBeVisible();
|
||||
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test('keeps apartment add and edit workflows usable', async ({ page }) => {
|
||||
const apartments = [
|
||||
{
|
||||
id: 'apt-1',
|
||||
name: 'T2 République',
|
||||
address: '12 rue de la République, Lyon',
|
||||
purchasePrice: 185000,
|
||||
description: 'Deuxième étage',
|
||||
},
|
||||
];
|
||||
|
||||
await mockUser(page, true);
|
||||
await page.route('**/api/apartments**', async (route) => {
|
||||
const request = route.request();
|
||||
const method = request.method();
|
||||
const id = new URL(request.url()).pathname.split('/').at(-1);
|
||||
|
||||
if (method === 'POST') {
|
||||
const apartment = { id: 'apt-2', ...request.postDataJSON() };
|
||||
apartments.push(apartment);
|
||||
await route.fulfill({ contentType: 'application/json', json: apartment });
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'PUT') {
|
||||
const index = apartments.findIndex((apartment) => apartment.id === id);
|
||||
apartments[index] = { id, ...request.postDataJSON() };
|
||||
await route.fulfill({ contentType: 'application/json', json: apartments[index] });
|
||||
return;
|
||||
}
|
||||
|
||||
if (method === 'DELETE') {
|
||||
const index = apartments.findIndex((apartment) => apartment.id === id);
|
||||
apartments.splice(index, 1);
|
||||
await route.fulfill({ status: 204 });
|
||||
return;
|
||||
}
|
||||
|
||||
await route.fulfill({ contentType: 'application/json', json: apartments });
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
await page
|
||||
.getByTestId('primary-navigation')
|
||||
.getByRole('button', { name: 'Appartements' })
|
||||
.click();
|
||||
await expect(page.getByRole('heading', { name: 'T2 République' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Modifier' }).click();
|
||||
await page.getByLabel("Nom de l'appartement").fill('T2 République rénové');
|
||||
await page.getByRole('button', { name: 'Enregistrer' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'T2 République rénové' })).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Ajouter un appartement' }).click();
|
||||
await page.getByLabel("Nom de l'appartement").fill('Studio Croix-Rousse');
|
||||
await page.getByLabel('Adresse complète').fill('8 montée de la Boucle, Lyon');
|
||||
await page.getByLabel("Prix d'acquisition").fill('125000');
|
||||
await page.getByRole('button', { name: 'Enregistrer' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'Studio Croix-Rousse' })).toBeVisible();
|
||||
|
||||
page.once('dialog', (dialog) => dialog.accept());
|
||||
const renovatedApartment = page
|
||||
.getByRole('article')
|
||||
.filter({ has: page.getByRole('heading', { name: 'T2 République rénové' }) });
|
||||
await renovatedApartment.getByRole('button', { name: 'Supprimer' }).click();
|
||||
await expect(page.getByRole('heading', { name: 'T2 République rénové' })).toBeHidden();
|
||||
|
||||
await expectNoHorizontalOverflow(page);
|
||||
});
|
||||
|
||||
test('keeps the secured API check and logout available', async ({ page }) => {
|
||||
await mockUser(page, true);
|
||||
await mockEmptyApartmentList(page);
|
||||
await page.route('**/api/hello', async (route) => {
|
||||
await route.fulfill({ contentType: 'text/plain', body: 'Service disponible' });
|
||||
});
|
||||
await page.route('**/logout', async (route) => {
|
||||
await route.fulfill({ contentType: 'text/html', body: '<h1>Signed out</h1>' });
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
await page.getByRole('button', { name: 'Tester la connexion' }).click();
|
||||
await expect(page.getByText('Service disponible')).toBeVisible();
|
||||
|
||||
await page.getByTestId('primary-navigation').getByRole('button', { name: 'Réglages' }).click();
|
||||
await page.getByTestId('session-settings').getByRole('button', { name: 'Déconnexion' }).click();
|
||||
await expect(page).toHaveURL(/\/logout$/);
|
||||
});
|
||||
|
||||
test('follows operating-system changes for the System theme', async ({ page }) => {
|
||||
await page.addInitScript(() => localStorage.setItem('lmnp-theme', 'system'));
|
||||
await page.emulateMedia({ colorScheme: 'dark' });
|
||||
await mockUser(page, true);
|
||||
await mockEmptyApartmentList(page);
|
||||
|
||||
await page.goto('/');
|
||||
await expect(page.locator('html')).toHaveAttribute('data-theme', 'dark');
|
||||
|
||||
await page.emulateMedia({ colorScheme: 'light' });
|
||||
await expect(page.locator('html')).toHaveAttribute('data-theme', 'light');
|
||||
});
|
||||
Reference in new issue
Block a user