試験結果
BIN
testing/recordings/correspondence_api_1772337223924.webp
Normal file
|
After Width: | Height: | Size: 9.6 MiB |
BIN
testing/recordings/dashboard_test_1772336335244.webp
Normal file
|
After Width: | Height: | Size: 190 KiB |
BIN
testing/recordings/fertilizer_test_1772336994610.webp
Normal file
|
After Width: | Height: | Size: 785 KiB |
BIN
testing/recordings/field_detail_test_1772336588494.webp
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
testing/recordings/login_flow_1772336274097.webp
Normal file
|
After Width: | Height: | Size: 768 KiB |
BIN
testing/recordings/reports_allocation_1772336843870.webp
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
testing/recordings/rules_reports_test_1772337104257.webp
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
testing/recordings/weather_page_test_1772336922550.webp
Normal file
|
After Width: | Height: | Size: 659 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 66 KiB |
BIN
testing/screenshots/02_fields/field_detail_top_1772336614836.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 132 KiB |
BIN
testing/screenshots/02_fields/fields_list_page_1772336436720.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 160 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 103 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 61 KiB |
BIN
testing/screenshots/08_mail/mail_history_page_1772337055980.png
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
testing/screenshots/08_mail/mail_rules_page_1772337122287.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 48 KiB |
BIN
testing/subagent_generated/e2e/screenshots/01-initial.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 48 KiB |
BIN
testing/subagent_generated/e2e/screenshots/02-after-login.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 70 KiB |
BIN
testing/subagent_generated/e2e/screenshots/03-404.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
BIN
testing/subagent_generated/e2e/screenshots/03-current-state.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 112 KiB |
BIN
testing/subagent_generated/e2e/screenshots/05-matrix-table.png
Normal file
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 113 KiB |
|
After Width: | Height: | Size: 112 KiB |
BIN
testing/subagent_generated/e2e/screenshots/B-table-header.png
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
BIN
testing/subagent_generated/e2e/screenshots/D-fertilizer-cell.png
Normal file
|
After Width: | Height: | Size: 283 B |
BIN
testing/subagent_generated/e2e/screenshots/f01-initial.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 70 KiB |
BIN
testing/subagent_generated/e2e/screenshots/f03-picker-open.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 119 KiB |
BIN
testing/subagent_generated/e2e/screenshots/f04-no-table.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
testing/subagent_generated/e2e/screenshots/f05-before-calc.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
testing/subagent_generated/e2e/screenshots/f06-after-calc.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 62 KiB |
BIN
testing/subagent_generated/e2e/screenshots/f09-final-full.png
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
testing/subagent_generated/e2e/screenshots/f09-final.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 70 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 75 KiB |
BIN
testing/subagent_generated/e2e/screenshots/step1_initial.png
Normal file
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 70 KiB |
BIN
testing/subagent_generated/e2e/screenshots/step3_fert_picker.png
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
testing/subagent_generated/e2e/screenshots/step4_with_matrix.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
|
After Width: | Height: | Size: 41 KiB |
35
testing/subagent_generated/playwright_debug.mjs
Normal file
@@ -0,0 +1,35 @@
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
import { mkdirSync } from 'fs';
|
||||
mkdirSync('C:/tmp/playwright_screenshots', { recursive: true });
|
||||
|
||||
// Navigate to login
|
||||
await page.goto('http://localhost:3000/login');
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.screenshot({ path: 'C:/tmp/playwright_screenshots/login_page.png', fullPage: true });
|
||||
|
||||
// Debug HTML form
|
||||
const formHTML = await page.locator('form').innerHTML().catch(() => 'no form found');
|
||||
console.log('Form HTML:', formHTML.substring(0, 1000));
|
||||
|
||||
const allInputs = await page.locator('input').all();
|
||||
console.log('Inputs count:', allInputs.length);
|
||||
for (const input of allInputs) {
|
||||
const name = await input.getAttribute('name');
|
||||
const type = await input.getAttribute('type');
|
||||
const placeholder = await input.getAttribute('placeholder');
|
||||
console.log(`Input: name=${name}, type=${type}, placeholder=${placeholder}`);
|
||||
}
|
||||
|
||||
const allButtons = await page.locator('button').all();
|
||||
for (const btn of allButtons) {
|
||||
const text = await btn.textContent();
|
||||
const type = await btn.getAttribute('type');
|
||||
console.log(`Button: text="${text?.trim()}", type=${type}`);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
58
testing/subagent_generated/playwright_full_test.mjs
Normal file
@@ -0,0 +1,58 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { mkdirSync } from 'fs';
|
||||
|
||||
const screenshotDir = 'C:/tmp/playwright_screenshots';
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
async function screenshot(name) {
|
||||
const path = `${screenshotDir}/${name}.png`;
|
||||
await page.screenshot({ path, fullPage: true });
|
||||
console.log(` [Screenshot saved: ${name}.png]`);
|
||||
return path;
|
||||
}
|
||||
|
||||
console.log('\n=== Step 1: Navigate to /fertilizer/new ===');
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log('URL:', page.url());
|
||||
|
||||
if (page.url().includes('/login')) {
|
||||
console.log('\n=== Step 2: Login ===');
|
||||
// Use the id-based selectors since name attribute is null
|
||||
await page.fill('#username', 'akira');
|
||||
await page.fill('input[type="password"]', 'keina2025');
|
||||
await screenshot('login_filled');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForNavigation({ timeout: 10000 }).catch(() => console.log('No navigation event'));
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log('After login URL:', page.url());
|
||||
|
||||
// Navigate to fertilizer/new
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log('After redirect URL:', page.url());
|
||||
}
|
||||
|
||||
await screenshot('step3_fertilizer_new');
|
||||
console.log('\n=== Step 3: Page content ===');
|
||||
const h1Text = await page.locator('h1, h2').first().textContent().catch(() => 'not found');
|
||||
console.log('Heading:', h1Text);
|
||||
|
||||
// List all select elements
|
||||
const selects = await page.locator('select').all();
|
||||
console.log('Select elements:', selects.length);
|
||||
for (const sel of selects) {
|
||||
const label = await sel.getAttribute('aria-label') || await sel.getAttribute('id') || 'no label';
|
||||
const options = await sel.locator('option').allTextContents();
|
||||
console.log(` Select [${label}]: options = ${options.slice(0, 10).join(', ')}`);
|
||||
}
|
||||
|
||||
// List all visible text to understand the page
|
||||
const allText = await page.locator('body').textContent();
|
||||
console.log('Page text (first 1000 chars):', allText?.substring(0, 1000));
|
||||
|
||||
await browser.close();
|
||||
76
testing/subagent_generated/playwright_full_test2.mjs
Normal file
@@ -0,0 +1,76 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { mkdirSync } from 'fs';
|
||||
|
||||
const screenshotDir = 'C:/tmp/playwright_screenshots';
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
async function screenshot(name) {
|
||||
const path = `${screenshotDir}/${name}.png`;
|
||||
await page.screenshot({ path, fullPage: false });
|
||||
console.log(` [Screenshot: ${name}.png]`);
|
||||
}
|
||||
|
||||
// Login
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
if (page.url().includes('/login')) {
|
||||
await page.fill('#username', 'akira');
|
||||
await page.fill('input[type="password"]', 'keina2025');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForNavigation({ timeout: 10000 }).catch(() => {});
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
// Step 3: Select にこまる
|
||||
console.log('\n=== Step 3: Select 品種 "にこまる" ===');
|
||||
const selects = await page.locator('select').all();
|
||||
await selects[1].selectOption({ label: 'にこまる' });
|
||||
await page.waitForTimeout(1500);
|
||||
await screenshot('step3_nikkomaru_selected');
|
||||
console.log('Selected にこまる - 15 fields auto-added');
|
||||
|
||||
// Step 4: Click + 肥料を追加 button
|
||||
console.log('\n=== Step 4: Click "+ 肥料を追加" ===');
|
||||
const addBtn = page.locator('button').filter({ hasText: '肥料を追加' }).first();
|
||||
await addBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Modal appeared - click グアノ
|
||||
console.log(' Modal open - clicking グアノ...');
|
||||
const guanoItem = page.locator('text=グアノ').first();
|
||||
await guanoItem.click();
|
||||
await page.waitForTimeout(1500);
|
||||
await screenshot('step4_guano_added');
|
||||
|
||||
// Check what appeared on the page
|
||||
const pageText = await page.locator('body').textContent();
|
||||
const cleanText = pageText?.replace(/\s+/g, ' ');
|
||||
const relevantPart = cleanText?.match(/自動計算設定.{0,500}/)?.[0] || cleanText?.substring(0, 600);
|
||||
console.log('After グアノ added:', relevantPart);
|
||||
|
||||
// Find the param input for グアノ
|
||||
console.log('\n=== Checking fertilizer section structure ===');
|
||||
const fertSection = page.locator('[class*="fertilizer"], [class*="Fertilizer"]').first();
|
||||
const fertHTML = await page.locator('body').innerHTML();
|
||||
// Look for the input near グアノ
|
||||
const inputs = await page.locator('input[type="number"], input[type="text"]').all();
|
||||
console.log('Number of inputs:', inputs.length);
|
||||
for (let i = 0; i < inputs.length; i++) {
|
||||
const val = await inputs[i].inputValue();
|
||||
const placeholder = await inputs[i].getAttribute('placeholder');
|
||||
const type = await inputs[i].getAttribute('type');
|
||||
console.log(` Input[${i}]: value="${val}", placeholder="${placeholder}", type="${type}"`);
|
||||
}
|
||||
|
||||
// Look for 計算 button
|
||||
const calcButton = page.locator('button').filter({ hasText: '計算' });
|
||||
const calcVisible = await calcButton.isVisible();
|
||||
console.log('計算 button visible:', calcVisible);
|
||||
|
||||
await browser.close();
|
||||
89
testing/subagent_generated/playwright_full_test3.mjs
Normal file
@@ -0,0 +1,89 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { mkdirSync } from 'fs';
|
||||
|
||||
const screenshotDir = 'C:/tmp/playwright_screenshots';
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
async function screenshot(name) {
|
||||
const path = `${screenshotDir}/${name}.png`;
|
||||
await page.screenshot({ path, fullPage: false });
|
||||
console.log(` [Screenshot: ${name}.png]`);
|
||||
}
|
||||
|
||||
// Login
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
if (page.url().includes('/login')) {
|
||||
await page.fill('#username', 'akira');
|
||||
await page.fill('input[type="password"]', 'keina2025');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForNavigation({ timeout: 10000 }).catch(() => {});
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
// Select にこまる
|
||||
const selects = await page.locator('select').all();
|
||||
await selects[1].selectOption({ label: 'にこまる' });
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Click + 肥料を追加
|
||||
const addBtn = page.locator('button').filter({ hasText: '肥料を追加' }).first();
|
||||
await addBtn.click();
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
// Click グアノ in modal
|
||||
await page.locator('text=グアノ').first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Step 5: Enter "3" in the param field and click 計算
|
||||
console.log('\n=== Step 5: Enter "3" in param field and click 計算 ===');
|
||||
|
||||
// The first input with placeholder "値" is the param field
|
||||
const paramInput = page.locator('input[placeholder="値"]');
|
||||
const paramVisible = await paramInput.isVisible();
|
||||
console.log('Param input visible:', paramVisible);
|
||||
|
||||
await paramInput.fill('3');
|
||||
await page.waitForTimeout(300);
|
||||
|
||||
const calcBtn = page.locator('button').filter({ hasText: '計算' });
|
||||
await calcBtn.click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await screenshot('step5_after_calc');
|
||||
|
||||
// Step 6: Check matrix cells - do they show decimal values in input fields?
|
||||
console.log('\n=== Step 6: Check matrix cell values BEFORE clicking ≈ ===');
|
||||
const allInputs = await page.locator('input[type="number"]').all();
|
||||
console.log('Number of number inputs:', allInputs.length);
|
||||
for (let i = 0; i < Math.min(allInputs.length, 20); i++) {
|
||||
const val = await allInputs[i].inputValue();
|
||||
const placeholder = await allInputs[i].getAttribute('placeholder');
|
||||
console.log(` Input[${i}]: value="${val}", placeholder="${placeholder}"`);
|
||||
}
|
||||
|
||||
// Step 7: Find and examine the グアノ column header button (≈ button)
|
||||
console.log('\n=== Step 7: Find ≈ button in グアノ column header ===');
|
||||
// Look for buttons with ≈ or similar content
|
||||
const allButtons = await page.locator('button').all();
|
||||
console.log('Total buttons on page:', allButtons.length);
|
||||
for (let i = 0; i < allButtons.length; i++) {
|
||||
const text = await allButtons[i].textContent();
|
||||
const cls = await allButtons[i].getAttribute('class');
|
||||
if (text && (text.includes('≈') || text.includes('↩') || text.includes('~') || text.trim().length <= 3)) {
|
||||
console.log(` Button[${i}]: text="${text?.trim()}", class="${cls?.substring(0, 100)}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get page text in the グアノ column area
|
||||
const bodyText = await page.locator('body').textContent();
|
||||
const guanoSection = bodyText?.match(/グアノ.{0,300}/)?.[0];
|
||||
console.log('グアノ section text:', guanoSection);
|
||||
|
||||
await browser.close();
|
||||
146
testing/subagent_generated/playwright_full_test4.mjs
Normal file
@@ -0,0 +1,146 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { mkdirSync } from 'fs';
|
||||
|
||||
const screenshotDir = 'C:/tmp/playwright_screenshots';
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
async function screenshot(name) {
|
||||
const path = `${screenshotDir}/${name}.png`;
|
||||
await page.screenshot({ path, fullPage: false });
|
||||
console.log(` [Screenshot: ${name}.png]`);
|
||||
}
|
||||
|
||||
// Login
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
if (page.url().includes('/login')) {
|
||||
await page.fill('#username', 'akira');
|
||||
await page.fill('input[type="password"]', 'keina2025');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForNavigation({ timeout: 10000 }).catch(() => {});
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
// Setup: Select にこまる, add グアノ, enter 3 and calculate
|
||||
const selects = await page.locator('select').all();
|
||||
await selects[1].selectOption({ label: 'にこまる' });
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator('button').filter({ hasText: '肥料を追加' }).first().click();
|
||||
await page.waitForTimeout(500);
|
||||
await page.locator('text=グアノ').first().click();
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator('input[placeholder="値"]').fill('3');
|
||||
await page.locator('button').filter({ hasText: '計算' }).click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
// Verify state before ≈ click
|
||||
console.log('\n=== BEFORE clicking ≈ ===');
|
||||
const inputsBefore = await page.locator('input[type="number"]').all();
|
||||
console.log('Matrix cell values (before ≈):');
|
||||
const valuesBefore = [];
|
||||
for (let i = 1; i < inputsBefore.length; i++) {
|
||||
const val = await inputsBefore[i].inputValue();
|
||||
valuesBefore.push(val);
|
||||
console.log(` Cell[${i}]: ${val}`);
|
||||
}
|
||||
|
||||
// Check the ≈ button
|
||||
const approxBtn = page.locator('button', { hasText: '≈' });
|
||||
const approxBtnClass = await approxBtn.getAttribute('class');
|
||||
console.log('\n≈ button class:', approxBtnClass);
|
||||
const approxBtnText = await approxBtn.textContent();
|
||||
console.log('≈ button text:', approxBtnText?.trim());
|
||||
|
||||
await screenshot('step6_before_approx_click');
|
||||
|
||||
// Step 8: Click ≈ button
|
||||
console.log('\n=== Step 8: Click ≈ button ===');
|
||||
await approxBtn.click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await screenshot('step9_after_approx_click');
|
||||
|
||||
// Step 9: Check values after ≈ click
|
||||
console.log('\n=== Step 9: Check values AFTER clicking ≈ ===');
|
||||
const inputsAfter = await page.locator('input[type="number"]').all();
|
||||
console.log('Matrix cell values (after ≈):');
|
||||
const valuesAfter = [];
|
||||
for (let i = 1; i < inputsAfter.length; i++) {
|
||||
const val = await inputsAfter[i].inputValue();
|
||||
valuesAfter.push(val);
|
||||
console.log(` Cell[${i}]: ${val}`);
|
||||
}
|
||||
|
||||
// Check for reference values (gray text) - might be in span or other elements
|
||||
// Look for elements that show original calc values as reference
|
||||
const refValueElements = await page.locator('[class*="gray"], [class*="text-gray"], [class*="ref"]').all();
|
||||
console.log('\nLooking for reference value indicators...');
|
||||
|
||||
// Check all visible text in the table area
|
||||
const tableText = await page.locator('table, [role="table"], .table-auto, [class*="table"]').first().textContent().catch(() => null);
|
||||
if (tableText) {
|
||||
console.log('Table text:', tableText.replace(/\s+/g, ' ').substring(0, 500));
|
||||
} else {
|
||||
// Try to get the section after the header
|
||||
const bodyText = await page.locator('body').textContent();
|
||||
const afterHeader = bodyText?.match(/圃場名.*面積.*グアノ.{0,1000}/s)?.[0];
|
||||
console.log('Matrix section:', afterHeader?.replace(/\s+/g, ' ').substring(0, 500));
|
||||
}
|
||||
|
||||
// Find the new button (should be ↩ now)
|
||||
const allButtonsAfter = await page.locator('button').all();
|
||||
console.log('\nLooking for ↩ button:');
|
||||
for (let i = 0; i < allButtonsAfter.length; i++) {
|
||||
const text = await allButtonsAfter[i].textContent();
|
||||
const cls = await allButtonsAfter[i].getAttribute('class');
|
||||
if (text && text.trim().length <= 3 && text.trim() !== '') {
|
||||
console.log(` Button[${i}]: text="${text?.trim()}", class="${cls?.substring(0, 100)}"`);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 10: Click ↩ button
|
||||
console.log('\n=== Step 10: Click ↩ button ===');
|
||||
const restoreBtn = page.locator('button').filter({ hasText: '↩' });
|
||||
const restoreBtnVisible = await restoreBtn.isVisible().catch(() => false);
|
||||
console.log('↩ button visible:', restoreBtnVisible);
|
||||
|
||||
if (restoreBtnVisible) {
|
||||
const restoreBtnClass = await restoreBtn.getAttribute('class');
|
||||
console.log('↩ button class:', restoreBtnClass);
|
||||
|
||||
await restoreBtn.click();
|
||||
await page.waitForTimeout(1500);
|
||||
|
||||
await screenshot('step11_after_restore');
|
||||
|
||||
// Step 11: Check values restored
|
||||
console.log('\n=== Step 11: Check values AFTER clicking ↩ ===');
|
||||
const inputsRestored = await page.locator('input[type="number"]').all();
|
||||
console.log('Matrix cell values (after ↩):');
|
||||
for (let i = 1; i < inputsRestored.length; i++) {
|
||||
const val = await inputsRestored[i].inputValue();
|
||||
console.log(` Cell[${i}]: ${val}`);
|
||||
}
|
||||
|
||||
// Check button is back to ≈
|
||||
const approxBtnRestored = page.locator('button', { hasText: '≈' });
|
||||
const approxVisible = await approxBtnRestored.isVisible().catch(() => false);
|
||||
const approxClass = await approxBtnRestored.getAttribute('class').catch(() => null);
|
||||
console.log('\n≈ button visible again:', approxVisible);
|
||||
console.log('≈ button class:', approxClass);
|
||||
} else {
|
||||
console.log('WARNING: ↩ button not found!');
|
||||
// Print all button texts for debugging
|
||||
for (let i = 0; i < allButtonsAfter.length; i++) {
|
||||
const text = await allButtonsAfter[i].textContent();
|
||||
console.log(` Button[${i}]: "${text?.trim()}"`);
|
||||
}
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
66
testing/subagent_generated/playwright_test_temp.mjs
Normal file
@@ -0,0 +1,66 @@
|
||||
import { chromium } from 'playwright';
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
const screenshotDir = 'C:/tmp/playwright_screenshots';
|
||||
// Create directory via node
|
||||
import { mkdirSync } from 'fs';
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
|
||||
function screenshot(name) {
|
||||
return page.screenshot({ path: `${screenshotDir}/${name}.png`, fullPage: true });
|
||||
}
|
||||
|
||||
// Step 1: Navigate to fertilizer/new
|
||||
console.log('\n=== Step 1: Navigating to http://localhost:3000/fertilizer/new ===');
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log('Current URL:', page.url());
|
||||
|
||||
// Check if redirected to login
|
||||
if (page.url().includes('/login')) {
|
||||
console.log('\n=== Step 2: Redirected to login, logging in... ===');
|
||||
|
||||
// Find and fill login form
|
||||
await page.waitForSelector('input', { timeout: 5000 });
|
||||
const inputs = await page.locator('input').all();
|
||||
console.log('Found inputs:', inputs.length);
|
||||
|
||||
// Try different selectors for username/password
|
||||
try {
|
||||
await page.fill('input[name="username"]', 'akira');
|
||||
} catch {
|
||||
await page.fill('input[type="text"]:first-of-type', 'akira');
|
||||
}
|
||||
|
||||
try {
|
||||
await page.fill('input[name="password"]', 'keina2025');
|
||||
} catch {
|
||||
await page.fill('input[type="password"]', 'keina2025');
|
||||
}
|
||||
|
||||
await screenshot('step2_login_form');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log('After login URL:', page.url());
|
||||
|
||||
// Navigate to fertilizer/new after login
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
console.log('After navigate URL:', page.url());
|
||||
}
|
||||
|
||||
await screenshot('step3_fertilizer_new');
|
||||
console.log('\n=== Step 3: On fertilizer/new page ===');
|
||||
console.log('URL:', page.url());
|
||||
|
||||
// Check page content
|
||||
const pageTitle = await page.title();
|
||||
console.log('Page title:', pageTitle);
|
||||
const bodyText = await page.locator('body').textContent();
|
||||
console.log('Body preview:', bodyText?.substring(0, 500));
|
||||
|
||||
await browser.close();
|
||||
console.log('\nDone!');
|
||||
97
testing/subagent_generated/playwright_ui_test.mjs
Normal file
@@ -0,0 +1,97 @@
|
||||
import { chromium } from 'playwright';
|
||||
import { mkdirSync } from 'fs';
|
||||
|
||||
const screenshotDir = 'C:/tmp/playwright_screenshots';
|
||||
mkdirSync(screenshotDir, { recursive: true });
|
||||
|
||||
const browser = await chromium.launch({ headless: true });
|
||||
const context = await browser.newContext({ viewport: { width: 1400, height: 900 } });
|
||||
const page = await context.newPage();
|
||||
|
||||
async function screenshot(name) {
|
||||
const path = `${screenshotDir}/${name}.png`;
|
||||
await page.screenshot({ path, fullPage: false });
|
||||
console.log(` [Screenshot: ${name}.png]`);
|
||||
}
|
||||
|
||||
async function screenshotFull(name) {
|
||||
const path = `${screenshotDir}/${name}.png`;
|
||||
await page.screenshot({ path, fullPage: true });
|
||||
console.log(` [Screenshot (full): ${name}.png]`);
|
||||
}
|
||||
|
||||
// Login flow
|
||||
console.log('\n=== Login ===');
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
if (page.url().includes('/login')) {
|
||||
await page.fill('#username', 'akira');
|
||||
await page.fill('input[type="password"]', 'keina2025');
|
||||
await page.click('button[type="submit"]');
|
||||
await page.waitForNavigation({ timeout: 10000 }).catch(() => {});
|
||||
await page.waitForLoadState('networkidle');
|
||||
await page.goto('http://localhost:3000/fertilizer/new');
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
console.log('On page:', page.url());
|
||||
|
||||
// Step 3: Select variety "にこまる"
|
||||
console.log('\n=== Step 3: Select 品種 "にこまる" ===');
|
||||
// Find the 品種 select (second select - first is year)
|
||||
const selects = await page.locator('select').all();
|
||||
console.log('Number of select elements:', selects.length);
|
||||
// The variety select should contain "にこまる"
|
||||
for (let i = 0; i < selects.length; i++) {
|
||||
const options = await selects[i].locator('option').allTextContents();
|
||||
console.log(`Select[${i}] options:`, options.join(', '));
|
||||
}
|
||||
|
||||
// Select にこまる
|
||||
await selects[1].selectOption({ label: 'にこまる' });
|
||||
await page.waitForTimeout(2000); // Wait for any async updates
|
||||
await screenshotFull('step3_variety_selected');
|
||||
console.log('Selected にこまる');
|
||||
|
||||
// Check if any fields were auto-added
|
||||
const allText = await page.locator('body').textContent();
|
||||
const relevantText = allText?.replace(/\s+/g, ' ').substring(0, 500);
|
||||
console.log('Page text after variety select:', relevantText);
|
||||
|
||||
// Step 4: Add fertilizer by clicking "+ 肥料を追加"
|
||||
console.log('\n=== Step 4: Click "+ 肥料を追加" ===');
|
||||
// Find the button
|
||||
const addBtn = page.locator('button').filter({ hasText: '肥料を追加' }).first();
|
||||
const addBtnVisible = await addBtn.isVisible();
|
||||
console.log('Add fertilizer button visible:', addBtnVisible);
|
||||
|
||||
if (addBtnVisible) {
|
||||
await addBtn.click();
|
||||
await page.waitForTimeout(1000);
|
||||
await screenshotFull('step4_after_add_click');
|
||||
|
||||
// Now we need to select "グアノ" from the new fertilizer row
|
||||
const allText2 = await page.locator('body').textContent();
|
||||
console.log('Page text after click:', allText2?.replace(/\s+/g, ' ').substring(0, 300));
|
||||
|
||||
// Check for any new select/dropdown that appeared
|
||||
const newSelects = await page.locator('select').all();
|
||||
console.log('Selects after click:', newSelects.length);
|
||||
|
||||
for (let i = 0; i < newSelects.length; i++) {
|
||||
const options = await newSelects[i].locator('option').allTextContents();
|
||||
if (options.some(o => o.includes('グアノ'))) {
|
||||
console.log(`Found グアノ in select[${i}], selecting...`);
|
||||
await newSelects[i].selectOption({ label: 'グアノ' });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
await screenshotFull('step4_guano_selected');
|
||||
|
||||
// Check page state
|
||||
const pageBody = await page.locator('body').textContent();
|
||||
console.log('After グアノ selection:', pageBody?.replace(/\s+/g, ' ').substring(0, 500));
|
||||
|
||||
await browser.close();
|
||||
4
testing/subagent_generated/test-results/.last-run.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
199
testing/subagent_generated/verify-fixes.spec.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import { test, expect, Page } from '@playwright/test';
|
||||
|
||||
const LOGIN_URL = 'http://localhost:3000/login';
|
||||
const API_URL = 'http://localhost:8000/api';
|
||||
const USERNAME = 'admin';
|
||||
const PASSWORD = 'admin123';
|
||||
|
||||
/** ログインしてトークンを localStorage にセット */
|
||||
async function loginViaUI(page: Page) {
|
||||
await page.goto(LOGIN_URL);
|
||||
await page.fill('#username', USERNAME);
|
||||
await page.fill('#password', PASSWORD);
|
||||
await page.click('button[type="submit"]');
|
||||
// ログイン後 /allocation にリダイレクトされるのを待つ
|
||||
await page.waitForURL('**/allocation', { timeout: 15000 });
|
||||
}
|
||||
|
||||
// ===== D-4: IsAuthenticated が有効か =====
|
||||
test.describe('D-4: IsAuthenticated', () => {
|
||||
test('未認証で API にアクセスすると 401 が返る', async ({ request }) => {
|
||||
const res = await request.get(`${API_URL}/fields/`);
|
||||
expect(res.status()).toBe(401);
|
||||
});
|
||||
|
||||
test('未認証でフロントにアクセスするとログイン画面にリダイレクト', async ({ page }) => {
|
||||
await page.goto('http://localhost:3000/allocation');
|
||||
// API が 401 を返すので、フロントがログイン画面に遷移するはず
|
||||
await page.waitForURL('**/login', { timeout: 10000 });
|
||||
await expect(page.locator('text=KeinaSystem')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ===== ログイン → 作付計画画面 =====
|
||||
test.describe('ログイン → 作付計画画面', () => {
|
||||
test('ログインして作付計画画面が表示される', async ({ page }) => {
|
||||
await loginViaUI(page);
|
||||
// allocation ページにいることを確認
|
||||
expect(page.url()).toContain('/allocation');
|
||||
// 圃場のデータが読み込まれるのを待つ(テーブルまたはリストが表示されるはず)
|
||||
await page.waitForTimeout(2000);
|
||||
// ページ内に何かしらのコンテンツがある
|
||||
const body = await page.textContent('body');
|
||||
expect(body).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// ===== A-8: 圃場詳細の共済/中山間情報表示 =====
|
||||
test.describe('A-8: 圃場詳細 共済/中山間情報', () => {
|
||||
test('共済・中山間の両方が紐づく圃場で情報が表示される', async ({ page }) => {
|
||||
await loginViaUI(page);
|
||||
// Field ID 8 (口神 ハウス南) は kyosai=1, chusankan=1
|
||||
await page.goto('http://localhost:3000/fields/8');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// 共済情報セクション
|
||||
await expect(page.locator('text=共済情報')).toBeVisible();
|
||||
// テーブルヘッダが表示される
|
||||
await expect(page.locator('text=耕地-分筆')).toBeVisible();
|
||||
await expect(page.locator('text=漢字地名')).toBeVisible();
|
||||
// 「紐づけられた共済区画はありません」が表示されないこと
|
||||
await expect(page.locator('text=紐づけられた共済区画はありません')).not.toBeVisible();
|
||||
|
||||
// 中山間情報セクション
|
||||
await expect(page.locator('text=中山間情報')).toBeVisible();
|
||||
await expect(page.locator('text=所在地')).toBeVisible();
|
||||
// 「紐づけられた中山間区画はありません」が表示されないこと
|
||||
await expect(page.locator('text=紐づけられた中山間区画はありません')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('共済のみ紐づく圃場で共済情報が表示される', async ({ page }) => {
|
||||
await loginViaUI(page);
|
||||
// Field ID 3 (おまけ) は kyosai=1, chusankan=0
|
||||
await page.goto('http://localhost:3000/fields/3');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await expect(page.locator('text=共済情報')).toBeVisible();
|
||||
await expect(page.locator('text=紐づけられた共済区画はありません')).not.toBeVisible();
|
||||
await expect(page.locator('text=紐づけられた中山間区画はありません')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
// ===== E-1: PDF帳票フォーマット再設計 =====
|
||||
test.describe('E-1: PDF帳票', () => {
|
||||
test('共済PDF が 200 で返る(表形式テンプレート)', async ({ page }) => {
|
||||
await loginViaUI(page);
|
||||
const token = await page.evaluate(() => localStorage.getItem('accessToken'));
|
||||
const res = await page.request.get(`${API_URL}/reports/kyosai/2025/`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(res.status()).toBe(200);
|
||||
expect(res.headers()['content-type']).toContain('application/pdf');
|
||||
});
|
||||
|
||||
test('中山間PDF が 200 で返る(表形式テンプレート)', async ({ page }) => {
|
||||
await loginViaUI(page);
|
||||
const token = await page.evaluate(() => localStorage.getItem('accessToken'));
|
||||
const res = await page.request.get(`${API_URL}/reports/chusankan/2025/`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(res.status()).toBe(200);
|
||||
expect(res.headers()['content-type']).toContain('application/pdf');
|
||||
});
|
||||
|
||||
test('帳票画面からPDFダウンロードリンクが表示される', async ({ page }) => {
|
||||
await loginViaUI(page);
|
||||
await page.goto('http://localhost:3000/reports');
|
||||
await page.waitForLoadState('networkidle');
|
||||
// 帳票画面が表示される
|
||||
const body = await page.textContent('body');
|
||||
expect(body).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
// ===== E-1c: 中山間マスタ 17フィールド =====
|
||||
test.describe('E-1c: 中山間マスタ拡張', () => {
|
||||
test('中山間 API が 17 フィールドを返す', async ({ page }) => {
|
||||
await loginViaUI(page);
|
||||
const token = await page.evaluate(() => localStorage.getItem('accessToken'));
|
||||
// Field ID 8 は chusankan=1
|
||||
const res = await page.request.get(`${API_URL}/fields/8/`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
expect(res.status()).toBe(200);
|
||||
const data = await res.json();
|
||||
const ch = data.chusankan_fields[0];
|
||||
expect(ch).toBeDefined();
|
||||
// 新フィールドが存在する
|
||||
expect(ch).toHaveProperty('manager');
|
||||
expect(ch).toHaveProperty('owner');
|
||||
expect(ch).toHaveProperty('planting_area');
|
||||
expect(ch).toHaveProperty('original_crop');
|
||||
expect(ch).toHaveProperty('slope');
|
||||
expect(ch).toHaveProperty('base_amount');
|
||||
expect(ch).toHaveProperty('branch_num');
|
||||
expect(ch).toHaveProperty('land_type');
|
||||
});
|
||||
|
||||
test('共済マスタの面積が 0 でない', async ({ page }) => {
|
||||
await loginViaUI(page);
|
||||
const token = await page.evaluate(() => localStorage.getItem('accessToken'));
|
||||
// Field ID 3 (おまけ) は kyosai=1
|
||||
const res = await page.request.get(`${API_URL}/fields/3/`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
const data = await res.json();
|
||||
const k = data.kyosai_fields[0];
|
||||
expect(k).toBeDefined();
|
||||
expect(k.area).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
// ===== C-2: 共済マスタ unique 制約 =====
|
||||
test.describe('C-2: 共済マスタ unique 制約 (k_num, s_num)', () => {
|
||||
test('同じ k_num + s_num の重複登録が拒否される', async ({ page }) => {
|
||||
await loginViaUI(page);
|
||||
const token = await page.evaluate(() => localStorage.getItem('accessToken'));
|
||||
|
||||
// まずテスト用データを作成
|
||||
const createRes = await page.request.post(`${API_URL}/fields/kyosai/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
k_num: 'TEST99',
|
||||
s_num: '1',
|
||||
kanji_name: 'テスト区画',
|
||||
address: 'テスト住所',
|
||||
area: 1000,
|
||||
},
|
||||
});
|
||||
|
||||
// 作成成功 or 既に存在する場合
|
||||
if (createRes.status() === 201) {
|
||||
// 同じ k_num + s_num で再度作成 → 拒否されるべき
|
||||
const dupRes = await page.request.post(`${API_URL}/fields/kyosai/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
data: {
|
||||
k_num: 'TEST99',
|
||||
s_num: '1',
|
||||
kanji_name: 'テスト区画2',
|
||||
address: 'テスト住所2',
|
||||
area: 2000,
|
||||
},
|
||||
});
|
||||
// 400 (validation error) が返るべき
|
||||
expect(dupRes.status()).toBe(400);
|
||||
|
||||
// クリーンアップ: テストデータ削除
|
||||
const created = await createRes.json();
|
||||
await page.request.delete(`${API_URL}/fields/kyosai/${created.id}/`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
159
testing/test_report.md
Normal file
@@ -0,0 +1,159 @@
|
||||
# KeinaSystem 読み取り専用テスト結果レポート
|
||||
|
||||
> **実施日**: 2026-03-01
|
||||
> **テスト方針**: データの書き込みを一切行わず、既存データの範囲内で画面表示・操作を検証
|
||||
|
||||
---
|
||||
|
||||
## テスト結果サマリー
|
||||
|
||||
| テスト対象 | 結果 | 備考 |
|
||||
|---|---|---|
|
||||
| ログイン | ✅ 正常 | ログイン後 `/allocation` にリダイレクト |
|
||||
| ダッシュボード | ✅ 正常 | サマリーカード・集計テーブル・クイックアクションすべて表示OK |
|
||||
| 圃場一覧 | ✅ 正常 | 39筆表示、並び替え・対応表切り替え動作OK |
|
||||
| 圃場詳細 | ✅ 正常 | 基本情報・共済1件・中山間1件の紐付き表示OK |
|
||||
| 圃場詳細(存在しないID) | ✅ 正常 | 「圃場が見つかりません」の適切なエラー表示 |
|
||||
| 対応表モード | ✅ 正常 | 共済/中山間の紐付き一覧が正しく表示 |
|
||||
| 作付け計画 | ✅ 正常 | 年度セレクタ・ソート・集計サイドバーすべて動作OK |
|
||||
| 作付け計画 フィルタ | ✅ 正常 | グループ順ソートが正しく動作 |
|
||||
| 帳票出力画面 | ✅ 正常 | 2種類の帳票表示OK |
|
||||
| 共済細目書PDFプレビュー | ⚠️ 軽微な問題 | 下記「発見事項」参照 |
|
||||
| 中山間申請書PDFプレビュー | ⚠️ 軽微な問題 | 下記「発見事項」参照 |
|
||||
| データ取込画面 | ✅ 正常 | 3種類のインポートセクション表示OK |
|
||||
| 気象データ(年別集計) | ✅ 正常 | サマリーカード・グラフ正常表示 |
|
||||
| 気象データ(月別サマリー) | ⚠️ 軽微な問題 | 下記「発見事項」参照 |
|
||||
| 気象データ(直近14日) | ✅ 正常 | 日次データテーブル正常表示 |
|
||||
| 気象データ(期間指定) | ✅ 正常 | 日付入力フィールド正常表示 |
|
||||
| 施肥計画一覧 | ✅ 正常 | 2026年度データなし → 「施肥計画はありません」表示OK |
|
||||
| 肥料マスタ | ✅ 正常 | 5件の肥料レコード正常表示 |
|
||||
| メール処理履歴 | ✅ 正常 | メール一覧・フィルタ・バッジ表示すべてOK |
|
||||
| メール通知ルール | ✅ 正常 | 6件のルール正常表示 |
|
||||
|
||||
---
|
||||
|
||||
## 発見された不具合・改善点
|
||||
|
||||
### 🟡 問題1: 気象データ月別サマリーのデータ未取得月の表示
|
||||
|
||||
**画面**: `/weather` → 月別サマリータブ
|
||||
**現象**: 2026年3月〜12月(まだデータがない月)で、最高気温・最低気温の列に「—°C」と表示される。
|
||||
**期待動作**: 「—」のみ表示するか、セルを空にすべき(「°C」単位がつくと0℃と紛らわしい)。
|
||||
**重要度**: 低(表示上の問題、データ自体は正しい)
|
||||
|
||||
**スクリーンショット**: `testing/screenshots/06_weather/weather_monthly_summary_*.png`
|
||||
|
||||
---
|
||||
|
||||
### 🟡 問題2: 水稲共済細目書PDFの紐付けなし行
|
||||
|
||||
**画面**: `/reports` → 水稲共済細目書プレビュー
|
||||
**現象**: 耕地番号 2-1 の行で、作付品目・品種・圃場名称がすべて「─」(ダッシュ)になっている。
|
||||
**原因の可能性**:
|
||||
- 共済マスタ耕地 2-1 に実圃場が紐付いていない
|
||||
- または紐付いてはいるが作付け計画が未設定
|
||||
**影響**: PDFの出力内容に空行が含まれる
|
||||
**重要度**: 中(実運用で提出するPDFに影響する可能性あり。ただし紐付けデータの問題であってシステムの不具合ではないかもしれない)
|
||||
|
||||
**スクリーンショット**: `testing/screenshots/04_reports/report_preview_pdf_*.png`
|
||||
|
||||
---
|
||||
|
||||
### 🟡 問題3: 中山間交付金申請書PDFの作物・品種・圃場名称
|
||||
|
||||
**画面**: `/reports` → 中山間交付金申請書プレビュー
|
||||
**現象**: ほぼすべての行で「作物」「品種」「圃場名称」の列が「─」(空)になっている。
|
||||
**原因の可能性**:
|
||||
- 中山間マスタの区画に実圃場が紐付いていない
|
||||
- 紐付いた圃場に2026年度の作付け計画がない
|
||||
- 中山間マスタは71区画あるが実圃場は39筆で、多くの区画が未紐付けの可能性
|
||||
**重要度**: 中(問題2と同じ原因の可能性が高い)
|
||||
|
||||
**スクリーンショット**: `testing/screenshots/04_reports/chusankan_report_preview_jap_*.png`
|
||||
|
||||
---
|
||||
|
||||
### 🔵 情報: コンソール警告
|
||||
|
||||
**全画面共通**: Next.js の Hydration 警告が出力されるケースがある。これは開発環境特有のもので、ブラウザ拡張機能による属性不一致が原因。システムの動作に影響なし。
|
||||
|
||||
**favicon.ico**: ダッシュボードで `favicon.ico` の 404 エラーがコンソールに記録されている。
|
||||
**重要度**: 最低(ユーザーに影響なし。本番環境でfaviconを設置すれば解消)
|
||||
|
||||
---
|
||||
|
||||
## 読み取り専用のため実施できなかったテスト一覧
|
||||
|
||||
以下のテストはデータの書き込みが必要なため、今回のテスト方針では実施不可でした。
|
||||
|
||||
### 圃場管理
|
||||
- [ ] 圃場の新規作成(`POST /api/fields/`)
|
||||
- [ ] 圃場情報の編集・保存(`PATCH /api/fields/{id}/`)
|
||||
- [ ] 圃場の削除(`DELETE /api/fields/{id}/`)
|
||||
- [ ] 共済マスタの紐付け追加(`POST /api/fields/{id}/kyosai-links/`)
|
||||
- [ ] 共済マスタの紐付け削除
|
||||
- [ ] 中山間マスタの紐付け追加・削除
|
||||
|
||||
### データ取込
|
||||
- [ ] 共済マスタODSファイルのインポート
|
||||
- [ ] 中山間マスタODSファイルのインポート
|
||||
- [ ] 実圃場ODSファイルのインポート
|
||||
|
||||
### 作付け計画
|
||||
- [ ] 作付け計画の作成(作物・品種の設定)
|
||||
- [ ] 作物のフィルタ(ドロップダウン変更後の表示確認)
|
||||
- [ ] 一括更新(チェックボックス選択→一括設定)
|
||||
- [ ] 前年度コピー
|
||||
- [ ] グループ名のインライン編集
|
||||
- [ ] 表示順の変更(↑↓ボタン)
|
||||
- [ ] 品種管理画面での追加・削除
|
||||
|
||||
### 帳票出力
|
||||
- [ ] PDFダウンロード(ダウンロードボタンのクリック)
|
||||
- [ ] CSVエクスポート(`GET /api/fields/export/zip/`)
|
||||
|
||||
### 施肥計画
|
||||
- [ ] 施肥計画の新規作成
|
||||
- [ ] 施肥計画の編集(マトリクス表入力)
|
||||
- [ ] 自動計算(per_tan / even / nitrogen の3方式)
|
||||
- [ ] 施肥計画PDF出力
|
||||
- [ ] 肥料マスタの新規追加・編集・削除
|
||||
|
||||
### メール通知
|
||||
- [ ] 送信者ルールの追加
|
||||
- [ ] 送信者ルールの削除
|
||||
- [ ] フィードバックの送信
|
||||
- [ ] メール履歴画面からのフィードバック編集
|
||||
|
||||
### 気象データ
|
||||
- [ ] 期間指定モードでの日付入力→表示(APIへのリクエスト発生)
|
||||
- [ ] 過去年度の選択(年セレクタで2016〜2025を選択)
|
||||
|
||||
---
|
||||
|
||||
## テスト環境
|
||||
|
||||
- **URL**: http://localhost:3000
|
||||
- **認証**: JWT(akira / keina2025)
|
||||
- **Docker**: docker-compose.yml(db + backend + frontend)
|
||||
- **テスト日時**: 2026-03-01 12:30〜13:15
|
||||
|
||||
---
|
||||
|
||||
## ファイル構成
|
||||
|
||||
```
|
||||
testing/
|
||||
├── test_report.md ← このファイル
|
||||
├── screenshots/
|
||||
│ ├── 01_dashboard/ ← ダッシュボード画面
|
||||
│ ├── 02_fields/ ← 圃場一覧・詳細・対応表
|
||||
│ ├── 03_allocation/ ← 作付け計画
|
||||
│ ├── 04_reports/ ← 帳票出力・PDFプレビュー
|
||||
│ ├── 05_import/ ← データ取込
|
||||
│ ├── 06_weather/ ← 気象データ
|
||||
│ ├── 07_fertilizer/ ← 施肥計画・肥料マスタ
|
||||
│ └── 08_mail/ ← メール履歴・ルール
|
||||
├── recordings/ ← ブラウザ操作の録画(WebP)
|
||||
└── subagent_generated/ ← テスト中に自動生成された一時ファイル
|
||||
```
|
||||