- Updated apartment-list component to use FontAwesome icons for better visual representation. - Refactored brand-mark component to utilize FontAwesome for the brand icon. - Enhanced dashboard-page with FontAwesome icons for improved navigation. - Replaced SVG icons with FontAwesome in settings-page for consistency and better performance. - Updated sign-in-page to use FontAwesome for the sign-in action icon. - Refactored tenant-card to incorporate FontAwesome icons for attachments. - Enhanced workspace-navigation with FontAwesome icons for all navigation items. - Applied consistent styling for fa-icon elements across components.
312 lines
12 KiB
TypeScript
312 lines
12 KiB
TypeScript
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,
|
|
);
|
|
for (const button of await navigation.locator('.navigation-item').all()) {
|
|
const buttonBox = await button.boundingBox();
|
|
const labelBox = await button.locator('.mdc-button__label > span').boundingBox();
|
|
expect(buttonBox).not.toBeNull();
|
|
expect(labelBox).not.toBeNull();
|
|
expect(labelBox!.x).toBeGreaterThanOrEqual(buttonBox!.x);
|
|
expect(labelBox!.x + labelBox!.width).toBeLessThanOrEqual(buttonBox!.x + buttonBox!.width);
|
|
}
|
|
}
|
|
|
|
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');
|
|
});
|
|
|
|
test('creates a tenant with an attachment and a lease with a guarantor', async ({ page }) => {
|
|
const apartments = [
|
|
{
|
|
id: 'apartment-1',
|
|
name: 'Studio République',
|
|
address: '12 rue de la République, Lyon',
|
|
purchasePrice: 150000,
|
|
description: '',
|
|
},
|
|
];
|
|
const tenants: Record<string, unknown>[] = [];
|
|
const leases: Record<string, unknown>[] = [];
|
|
|
|
await mockUser(page, true);
|
|
await page.route('**/api/apartments', (route) =>
|
|
route.fulfill({ contentType: 'application/json', json: apartments }),
|
|
);
|
|
await page.route('**/api/tenants**', async (route) => {
|
|
const request = route.request();
|
|
const path = new URL(request.url()).pathname;
|
|
if (request.method() === 'POST' && path.endsWith('/attachments')) {
|
|
const attachment = {
|
|
id: 'attachment-1',
|
|
fileName: 'identite.pdf',
|
|
contentType: 'application/pdf',
|
|
size: 8,
|
|
};
|
|
(tenants[0]['attachments'] as Record<string, unknown>[]).push(attachment);
|
|
await route.fulfill({ contentType: 'application/json', json: attachment });
|
|
return;
|
|
}
|
|
if (request.method() === 'POST') {
|
|
const tenant = { id: 'tenant-1', attachments: [], ...request.postDataJSON() };
|
|
tenants.push(tenant);
|
|
await route.fulfill({ contentType: 'application/json', json: tenant });
|
|
return;
|
|
}
|
|
await route.fulfill({ contentType: 'application/json', json: tenants });
|
|
});
|
|
await page.route('**/api/leases**', async (route) => {
|
|
const request = route.request();
|
|
const path = new URL(request.url()).pathname;
|
|
if (request.method() === 'GET' && path.endsWith('/pdf')) {
|
|
await route.fulfill({
|
|
contentType: 'application/pdf',
|
|
headers: { 'Content-Disposition': 'attachment; filename="bail-lease-1.pdf"' },
|
|
body: '%PDF-1.4\n%%EOF',
|
|
});
|
|
return;
|
|
}
|
|
if (request.method() === 'POST') {
|
|
const lease = { id: 'lease-1', ...request.postDataJSON() };
|
|
leases.push(lease);
|
|
await route.fulfill({ contentType: 'application/json', json: lease });
|
|
return;
|
|
}
|
|
await route.fulfill({ contentType: 'application/json', json: leases });
|
|
});
|
|
|
|
await page.goto('/');
|
|
await page.getByTestId('primary-navigation').getByRole('button', { name: 'Locataires' }).click();
|
|
await page.getByRole('button', { name: 'Ajouter un locataire' }).first().click();
|
|
await page.getByLabel('Prénom', { exact: true }).fill('Camille');
|
|
await page.getByLabel('Nom', { exact: true }).fill('Martin');
|
|
await page.getByLabel('E-mail', { exact: true }).fill('camille@example.com');
|
|
await page.getByLabel('Téléphone', { exact: true }).fill('+33612345678');
|
|
await page.getByLabel('Adresse complète').fill('10 rue des Lilas, Lyon');
|
|
await page.getByRole('button', { name: 'Enregistrer' }).click();
|
|
await expect(page.getByRole('heading', { name: 'Camille Martin' })).toBeVisible();
|
|
|
|
await page.getByLabel('Ajouter une pièce jointe pour Camille Martin').setInputFiles({
|
|
name: 'identite.pdf',
|
|
mimeType: 'application/pdf',
|
|
buffer: Buffer.from('identity'),
|
|
});
|
|
await expect(
|
|
page.getByRole('button', { name: 'Télécharger la pièce jointe identite.pdf' }),
|
|
).toBeVisible();
|
|
|
|
await page.getByTestId('primary-navigation').getByRole('button', { name: 'Baux' }).click();
|
|
await page.getByRole('button', { name: 'Créer un bail' }).first().click();
|
|
await page.getByRole('combobox', { name: 'Appartement' }).click();
|
|
await page.getByRole('option', { name: 'Studio République' }).click();
|
|
await page.getByRole('combobox', { name: 'Locataires' }).click();
|
|
await page.getByRole('option', { name: 'Camille Martin' }).click();
|
|
await page.keyboard.press('Escape');
|
|
await page.getByLabel('Date de début').fill('2026-09-01');
|
|
await page.getByLabel('Date de fin').fill('2027-08-31');
|
|
await page.getByLabel('Loyer mensuel').fill('850');
|
|
await page.getByLabel('Charges mensuelles').fill('50');
|
|
await page.getByLabel('Dépôt de garantie').fill('850');
|
|
await page.getByLabel('Prénom du garant').fill('Alex');
|
|
await page.getByLabel('Nom du garant', { exact: true }).fill('Durand');
|
|
await page.getByLabel('E-mail du garant').fill('alex@example.com');
|
|
await page.getByRole('button', { name: 'Ajouter le garant' }).click();
|
|
await page.getByRole('button', { name: 'Enregistrer le bail' }).click();
|
|
|
|
await expect(page.getByRole('heading', { name: 'Studio République' })).toBeVisible();
|
|
await expect(page.getByText('Alex Durand')).toBeVisible();
|
|
const downloadPromise = page.waitForEvent('download');
|
|
await page.getByRole('button', { name: 'Télécharger le bail' }).click();
|
|
const download = await downloadPromise;
|
|
expect(download.suggestedFilename()).toBe('bail-lease-1.pdf');
|
|
await expectNoHorizontalOverflow(page);
|
|
});
|