試験結果

This commit is contained in:
Akira
2026-03-01 13:42:13 +09:00
parent cfd67e0d55
commit b855608084
82 changed files with 929 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 768 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 659 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View 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();

View 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();

View 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();

View 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();

View 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();

View 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!');

View 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();

View File

@@ -0,0 +1,4 @@
{
"status": "passed",
"failedTests": []
}

View 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
View 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
- **認証**: JWTakira / keina2025
- **Docker**: docker-compose.ymldb + 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/ ← テスト中に自動生成された一時ファイル
```