試験結果
4
testing/subagent_generated/.last-run.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"status": "passed",
|
||||
"failedTests": []
|
||||
}
|
||||
|
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 |
199
testing/subagent_generated/e2e/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}` },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
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}` },
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||