import { expect, test, type Page } from '@playwright/test'; async function login(page: Page, username = 'mobile-admin', target = '/admin/users') { await page.addInitScript(() => localStorage.setItem('pipu.locale', 'ru')); await page.goto(`/login?redirect=${encodeURIComponent(target)}`); await page.getByLabel('Логин', { exact: true }).fill(username); await page.getByRole('textbox', { name: 'Пароль', exact: true }).fill(`${username}-password`); await page.getByRole('button', { name: 'Войти', exact: true }).click(); await expect(page.getByRole('button', { name: 'Меню профиля' })).toBeVisible(); } for (const width of [320, 1280]) { test(`user transfer and cover counts at ${width}px`, async ({ page }, info) => { await page.setViewportSize({ width, height: 900 }); await login(page); await page.evaluate(() => { const original = window.fetch; window.fetch = async (input, init) => { const req = new Request(input, init); const path = new URL(req.url).pathname; if (path === '/api/v1/admin/users/export') return new Response(JSON.stringify({ version: 1, users: [{ username: 'alice', role: 'user', password_hash: '$argon2id$test' }], }), { headers: { 'Content-Type': 'application/json' } }); if (path === '/api/v1/admin/users/import') { const archive = await req.json(); if (archive.users[0].password_hash !== '$argon2id$test') throw new Error('lost hash'); return new Response(JSON.stringify({ created: 1, skipped: ['alice'] }), { headers: { 'Content-Type': 'application/json' } }); } return original(input, init); }; }); const downloadPromise = page.waitForEvent('download'); await page.getByRole('button', { name: 'Экспорт пользователей', exact: true }).click(); const download = await downloadPromise; expect(download.suggestedFilename()).toBe('pipu-users.json'); await page.getByRole('button', { name: 'Импорт пользователей', exact: true }).click(); const dialog = page.getByRole('dialog', { name: 'Импорт пользователей' }); const file = dialog.getByLabel('Файл пользователей (.json)'); await file.setInputFiles({ name: 'bad.json', mimeType: 'application/json', buffer: Buffer.from('{}') }); await expect(dialog.getByRole('alert')).toContainText('Некорректный файл'); await expect(dialog.getByRole('button', { name: 'Импортировать', exact: true })).toBeDisabled(); await file.setInputFiles({ name: 'users.json', mimeType: 'application/json', buffer: Buffer.from(JSON.stringify({ version: 1, users: [{ username: 'new-user', role: 'moderator', password_hash: '$argon2id$test' }], })) }); await expect(dialog).toContainText('Учётных записей в файле: 1'); await page.screenshot({ path: info.outputPath('user-import.png') }); await dialog.getByRole('button', { name: 'Импортировать', exact: true }).click(); await expect(dialog.getByRole('status')).toContainText('Создано: 1. Пропущено существующих: 1.'); await expect(dialog.getByRole('status')).toContainText('alice'); await page.keyboard.press('Escape'); if (width < 768) { await page.getByRole('button', { name: 'Ещё', exact: true }).click(); await page.getByRole('dialog').getByRole('link', { name: 'Хранилища', exact: true }).click(); } else await page.evaluate(() => { history.pushState({}, '', '/admin/sources'); window.dispatchEvent(new PopStateEvent('popstate')); }); await expect(page.getByRole('article')).toContainText('Локальные обложки: 87 из 120 файлов'); await expect(page.getByRole('article')).toContainText('Альбомов ожидают обработки: 3'); await page.screenshot({ path: info.outputPath('source-covers.png') }); await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth - innerWidth)).toBeLessThanOrEqual(1); }); } test('moderator cannot see user transfer controls', async ({ page }) => { await login(page, 'moderator'); await expect(page.getByRole('heading', { name: 'Пользователи', exact: true })).toBeVisible(); await expect(page.getByRole('button', { name: 'Экспорт пользователей', exact: true })).toHaveCount(0); await expect(page.getByRole('button', { name: 'Импорт пользователей', exact: true })).toHaveCount(0); });