# Instructions - Following Playwright test failed. - Explain why, be concise, respect Playwright best practices. - Provide a snippet of code with the fix, if possible. # Test info - Name: requests.test.ts >> [REQ-001] [REQ-006] возвращённая исправляется новой pending, исходная сохранена - Location: tests/e2e/requests.test.ts:139:1 # Error details ``` Error: expect(locator).toBeVisible() failed Locator: getByText('На доработке', { exact: true }) Expected: visible Timeout: 5000ms Error: element(s) not found Call log: - Expect "toBeVisible" getByText('На доработке', { exact: true }) with timeout 5000ms - waiting for getByText('На доработке', { exact: true }) ``` ```yaml - banner: - link "Лоция, ИТ-компания": - /url: / - text: Ресурсное планирование - main ``` # Test source ```ts 41 | await page.getByLabel('Комментарий', { exact: true }).fill(`Решение ${action}`); 42 | const saved = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/api/v1/requests')); 43 | await page.getByRole('button', { name: 'Отправить на согласование', exact: true }).click(); 44 | const request = RequestView.parse(await (await saved).json()); ids.push(request.id); 45 | await expect(page.getByText('На согласовании', { exact: true })).toBeVisible(); 46 | await expect(page.getByRole('button', { name: /^(Согласовать|На доработку|Отклонить)$/ })).toHaveCount(0); 47 | } 48 | await page.getByRole('button', { name: 'Выйти', exact: true }).click(); await login(page, '3'); 49 | await page.goto(`/requests/${ids[0]}`); 50 | await page.getByLabel('Назначить сотрудника · Ресурс 1', { exact: true }).selectOption({ label: 'Анна Разработчик' }); 51 | await page.screenshot({ path: `.test-results/request-decisions-${test.info().project.name}.png`, fullPage: true }); 52 | await page.getByRole('button', { name: 'Согласовать', exact: true }).click(); 53 | await expect(page.getByText('Согласована', { exact: true })).toBeVisible(); 54 | await expect(page.getByRole('button', { name: 'Согласовать', exact: true })).toHaveCount(0); 55 | await page.getByRole('link', { name: 'Перейти к плану', exact: true }).click(); 56 | await page.getByLabel('Первый месяц', { exact: true }).selectOption('2026-10'); 57 | await expect(page.locator('[data-assignment]')).toHaveCount(1); 58 | await expect(page.locator('[data-assignment] td').nth(1)).toHaveText('0,25'); 59 | await page.screenshot({ path: `.test-results/request-decision-plan-${test.info().project.name}.png`, fullPage: true }); 60 | for (const [index, label, status] of [[1, 'На доработку', 'На доработке'], [2, 'Отклонить', 'Отклонена']] as const) { 61 | await page.goto(`/requests/${ids[index]}`); await page.getByRole('button', { name: label, exact: true }).click(); 62 | await expect(page.getByRole('alert')).toContainText('Укажите причину'); 63 | await page.getByLabel('Комментарий к решению', { exact: true }).fill('Уточнить загрузку'); 64 | await page.getByRole('button', { name: label, exact: true }).click(); 65 | await expect(page.getByText(status, { exact: true })).toBeVisible(); 66 | await expect(page.getByRole('button', { name: label, exact: true })).toHaveCount(0); 67 | expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); 68 | } 69 | }); 70 | test('[REQ-009] серверные часы и доход сотрудника совпадают в периоде формы, карточке и списке', async ({ page }) => { 71 | await seed(page); await login(page); await newForm(page); 72 | await page.getByLabel('Количество месяцев', { exact: true }).selectOption('3'); 73 | await page.getByLabel('Роль · Ресурс 1', { exact: true }).selectOption('backend'); 74 | await page.getByLabel('Желаемый сотрудник · Ресурс 1', { exact: true }).selectOption({ label: 'Анна Разработчик' }); 75 | await page.getByLabel('FTE · 2026-10 · Ресурс 1', { exact: true }).fill('0,33333'); 76 | await page.getByLabel('FTE · 2026-11 · Ресурс 1', { exact: true }).fill('0,5'); 77 | await page.getByLabel('FTE · 2026-12 · Ресурс 1', { exact: true }).fill('0,5'); 78 | await expect(page.getByTestId('request-total')).toContainText('904 664,32'); 79 | const period = page.getByRole('region', { name: 'Период · Ресурс 1', exact: true }); 80 | const figures = await period.locator('tbody tr').evaluateAll(rows => rows.map(row => Array.from(row.querySelectorAll('td')).slice(1).map(cell => cell.textContent))); 81 | expect(figures).toEqual([['58,67', '234 664,32 ₽'], ['79,5', '318 000,00 ₽'], ['88', '352 000,00 ₽']]); 82 | await expect(page.getByText('Часы и доход сотрудника неизвестны', { exact: false })).toHaveCount(0); 83 | await page.screenshot({ path: `.test-results/requests-preview-form-${test.info().project.name}.png`, fullPage: true }); 84 | await page.getByRole('button', { name: 'Отправить на согласование', exact: true }).click(); 85 | await expect(page.getByRole('heading', { name: 'Заявка на ресурсы', exact: true })).toBeVisible(); 86 | await expect(period.locator('tbody tr')).toHaveCount(figures.length); 87 | expect(await period.locator('tbody tr').evaluateAll(rows => rows.map(row => Array.from(row.querySelectorAll('td')).slice(1).map(cell => cell.textContent)))).toEqual(figures); 88 | await expect(page.getByText('Часы: 226,17 · Доход: 904 664,32 ₽', { exact: true })).toBeVisible(); 89 | const id = new URL(page.url()).pathname.split('/').at(-1); 90 | await page.screenshot({ path: `.test-results/requests-preview-card-${test.info().project.name}.png`, fullPage: true }); 91 | await page.getByRole('link', { name: 'К списку заявок', exact: true }).click(); 92 | const item = page.locator(`tr[data-request="${id}"]`); 93 | await expect(item).toContainText('904 664 ₽'); 94 | const listed = RequestPage.parse(await (await page.request.get('/api/v1/requests')).json()).items.find(request => request.id === id); 95 | expect(listed?.totals).toEqual({ hours: '226.16608', income: '904664.32' }); 96 | expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); 97 | }); 98 | test('[REQ-001] РП создаёт две строки, видит список и карточку, другой РП того же проекта их не видит', async ({ page }) => { 99 | await seed(page); await login(page); await newForm(page); 100 | await page.getByLabel('FTE · 2026-10 · Ресурс 1', { exact: true }).fill('0,5'); 101 | await expect(page.getByTestId('request-total')).toContainText('352 000'); 102 | await page.getByRole('button', { name: 'Добавить ресурс', exact: true }).click(); 103 | await page.getByLabel('Роль · Ресурс 2', { exact: true }).selectOption('frontend'); 104 | await page.getByLabel('Доходная ставка ₽/час · Ресурс 2', { exact: true }).fill('5000'); 105 | await page.getByLabel('FTE · 2026-10 · Ресурс 2', { exact: true }).fill('0,25'); 106 | const comment = `Две строки ${test.info().project.name}`; 107 | await page.getByLabel('Комментарий', { exact: true }).fill(comment); 108 | const accepted = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/api/v1/requests')); 109 | await page.getByRole('button', { name: 'Отправить на согласование', exact: true }).click(); 110 | const response = await accepted; expect(response.status()).toBe(201); 111 | const request = RequestView.parse(await response.json()); expect(request.lines).toHaveLength(2); 112 | await expect(page.getByRole('heading', { name: 'Заявка на ресурсы', exact: true })).toBeVisible(); 113 | await expect(page.getByText(comment, { exact: true })).toBeVisible(); 114 | expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); 115 | await page.screenshot({ path: `.test-results/requests-card-${test.info().project.name}.png`, fullPage: true }); 116 | await page.getByRole('link', { name: 'К списку заявок', exact: true }).click(); 117 | await expect(page.locator(`a[href$="/requests/${request.id}"]`)).toBeVisible(); 118 | await page.getByLabel('Статус', { exact: true }).selectOption('returned'); 119 | await expect(page.locator(`a[href$="/requests/${request.id}"]`)).toHaveCount(0); 120 | await page.getByLabel('Статус', { exact: true }).selectOption('pending'); 121 | await expect(page.locator(`a[href$="/requests/${request.id}"]`)).toBeVisible(); 122 | await page.screenshot({ path: `.test-results/requests-list-${test.info().project.name}.png`, fullPage: true }); 123 | await page.getByRole('button', { name: 'Выйти', exact: true }).click(); await login(page, '8'); 124 | await expect(page.getByText('Заявок пока нет', { exact: true })).toBeVisible(); 125 | await page.goto(`/requests/${request.id}`); 126 | await expect(page.getByText('Заявка не найдена', { exact: true })).toBeVisible(); 127 | }); 128 | test('[REQ-001] [REQ-002] все FTE равны нулю — понятная ошибка без сохранения', async ({ page }) => { 129 | await seed(page); await login(page); 130 | const before = RequestPage.parse(await (await page.request.get('/api/v1/requests')).json()); 131 | await newForm(page); await page.getByLabel('FTE · 2026-10 · Ресурс 1', { exact: true }).fill('0'); 132 | await page.getByRole('button', { name: 'Отправить на согласование', exact: true }).click(); 133 | await expect(page.getByText('Укажите хотя бы один месяц с ненулевой FTE', { exact: true })).toBeVisible(); 134 | expect(RequestPage.parse(await (await page.request.get('/api/v1/requests')).json())).toEqual(before); 135 | await expect(page.getByLabel('Доходная ставка ₽/час · Ресурс 1', { exact: true })).toHaveValue('4000'); 136 | expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); 137 | await page.screenshot({ path: `.test-results/requests-error-${test.info().project.name}.png`, fullPage: true }); 138 | }); 139 | test('[REQ-001] [REQ-006] возвращённая исправляется новой pending, исходная сохранена', async ({ page }) => { 140 | const id = await seed(page, true); await login(page); await page.goto(`/requests/${id}`); > 141 | await expect(page.getByText('На доработке', { exact: true })).toBeVisible(); | ^ Error: expect(locator).toBeVisible() failed 142 | await page.getByRole('link', { name: 'Исправить', exact: true }).click(); 143 | await expect(page.getByLabel('Доходная ставка ₽/час · Ресурс 1', { exact: true })).toHaveValue('4000.00'); 144 | await page.getByLabel('Комментарий', { exact: true }).fill('Исправленная заявка'); 145 | await page.screenshot({ path: `.test-results/requests-form-${test.info().project.name}.png`, fullPage: true }); 146 | const accepted = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/api/v1/requests')); 147 | await page.getByRole('button', { name: 'Отправить на согласование', exact: true }).click(); 148 | const response = await accepted; expect(response.status()).toBe(201); 149 | const updated = RequestView.parse(await response.json()); expect(updated.id).not.toBe(id); expect(updated.status).toBe('pending'); 150 | expect(RequestView.parse(await (await page.request.get(`/api/v1/requests/${id}`)).json()).status).toBe('returned'); 151 | }); 152 | test('[REQ-009] [REQ-032] РМ видит расход и «Выше доходной» в согласованной заявке, РП расхода не видит', async ({ page }) => { 153 | await seed(page); await login(page); await newForm(page); 154 | await page.getByLabel('Доходная ставка ₽/час · Ресурс 1', { exact: true }).fill('2500'); 155 | await page.getByLabel('Роль · Ресурс 1', { exact: true }).selectOption('backend'); 156 | await page.getByLabel('FTE · 2026-10 · Ресурс 1', { exact: true }).fill('0,5'); 157 | await expect(page.getByTestId('request-total')).toContainText('220 000,00'); 158 | await expect(page.locator('.rp-requests').getByText('Расход', { exact: false })).toHaveCount(0); 159 | const saved = page.waitForResponse(response => response.request().method() === 'POST' && response.url().endsWith('/api/v1/requests')); 160 | await page.getByRole('button', { name: 'Отправить на согласование', exact: true }).click(); 161 | const id = RequestView.parse(await (await saved).json()).id; 162 | await expect(page.getByText('На согласовании', { exact: true })).toBeVisible(); 163 | await page.getByRole('button', { name: 'Выйти', exact: true }).click(); await login(page, '3'); 164 | await page.goto(`/requests/${id}`); 165 | await page.getByLabel('Назначить сотрудника · Ресурс 1', { exact: true }).selectOption({ label: 'Анна Разработчик' }); 166 | await page.getByRole('button', { name: 'Согласовать', exact: true }).click(); 167 | await page.getByLabel('Подтверждаю превышение расходов над доходом', { exact: true }).check(); 168 | await page.getByRole('button', { name: 'Согласовать', exact: true }).click(); 169 | await expect(page.getByText('Согласована', { exact: true })).toBeVisible(); 170 | await expect(page.getByText('Плановый расход за период: 264 000,00 ₽', { exact: true })).toBeVisible(); 171 | await expect(page.getByText('Расходная ставка ₽/час: 3 000,00 ₽', { exact: true })).toBeVisible(); 172 | await expect(page.getByText('Выше доходной · Δ 500,00 ₽/час · превышение за период 44 000,00 ₽', { exact: true })).toBeVisible(); 173 | await expect(page.getByText(/^Разница: [-−]44\s000,00\s₽$/)).toHaveClass(/rp-loss/); 174 | const period = page.getByRole('region', { name: 'Период · Ресурс 1', exact: true }); 175 | await expect(period.locator('thead th')).toHaveText(['Месяц', 'FTE', 'Доходная', 'Расходная', 'Часы', 'Доход', 'Расход']); 176 | await expect(period.locator('table')).toHaveCSS('min-width', '760px'); 177 | expect((await period.locator('tbody td').allTextContents()).map(cell => cell.replace(/\s/g, ' '))).toEqual(['0,5', '2 500,00 ₽', '3 000,00 ₽', '88', '220 000,00 ₽', '264 000,00 ₽']); 178 | expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); 179 | await page.screenshot({ path: `.test-results/requests-cost-card-${test.info().project.name}.png`, fullPage: true }); 180 | await page.getByRole('link', { name: 'К списку заявок', exact: true }).click(); 181 | const item = page.locator(`tr[data-request="${id}"]`); 182 | await expect(page.locator('.rp-requests-table thead th')).toHaveText(['Проект и руководитель', 'Период', 'Доход', 'Расход', 'Статус', 'Действия']); 183 | await expect(item.locator('td').nth(3)).toHaveText('264 000 ₽'); await expect(item.locator('td').nth(3).locator('.rp-cell-over')).toHaveCount(1); 184 | await page.getByRole('button', { name: 'Выйти', exact: true }).click(); await login(page); 185 | await page.goto(`/requests/${id}`); 186 | await expect(page.getByText('Согласована', { exact: true })).toBeVisible(); 187 | await expect(page.locator('.rp-requests').getByText(/Расход|Разница|Выше доходной/)).toHaveCount(0); 188 | await expect(page.locator('.rp-months thead th')).toHaveText(['Месяц', 'FTE', 'Часы', 'Доход']); 189 | }); 190 | ```