init
This commit is contained in:
@@ -0,0 +1,446 @@
|
||||
/**
|
||||
* ImageAttachment Component Property-Based Tests
|
||||
* Feature: accounting-feature-upgrade
|
||||
* Property 8: Image deletion consistency
|
||||
* Validates: Requirements 4.7
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, afterEach } from 'vitest';
|
||||
import { render, screen, fireEvent, cleanup } from '@testing-library/react';
|
||||
import fc from 'fast-check';
|
||||
import { ImageAttachment } from './ImageAttachment';
|
||||
import type { TransactionImage } from '../../../types';
|
||||
|
||||
describe('ImageAttachment Property Tests', () => {
|
||||
// Clean up after each test to avoid DOM pollution
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
/**
|
||||
* Property 8: Image deletion consistency
|
||||
* For any image list and any delete operation, after deletion:
|
||||
* - The image should be removed from the list
|
||||
* - The list length should decrease by 1
|
||||
*
|
||||
* **Validates: Requirements 4.7**
|
||||
*/
|
||||
it('Property 8: should maintain deletion consistency', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
// Generate array of 1-9 images
|
||||
fc.array(
|
||||
fc.record({
|
||||
id: fc.integer({ min: 1, max: 10000 }),
|
||||
transactionId: fc.constant(1),
|
||||
filePath: fc.string(),
|
||||
fileName: fc.string(),
|
||||
fileSize: fc.integer({ min: 1, max: 10485760 }),
|
||||
mimeType: fc.constantFrom('image/jpeg', 'image/png', 'image/heic'),
|
||||
createdAt: fc.constant('2024-01-01T00:00:00Z'),
|
||||
}),
|
||||
{ minLength: 1, maxLength: 9 }
|
||||
),
|
||||
// Generate index to delete
|
||||
fc.nat(),
|
||||
(images, deleteIndexRaw) => {
|
||||
// Clean up before each iteration
|
||||
cleanup();
|
||||
|
||||
// Ensure unique IDs
|
||||
const uniqueImages = images.map((img, idx) => ({
|
||||
...img,
|
||||
id: idx + 1,
|
||||
}));
|
||||
|
||||
const deleteIndex = deleteIndexRaw % uniqueImages.length;
|
||||
const imageToDelete = uniqueImages[deleteIndex];
|
||||
|
||||
// Track deletion
|
||||
let deletedImageId: number | null = null;
|
||||
const mockOnRemove = (imageId: number) => {
|
||||
deletedImageId = imageId;
|
||||
};
|
||||
|
||||
const mockOnAdd = vi.fn();
|
||||
const mockOnPreview = vi.fn();
|
||||
|
||||
// Render component
|
||||
const { rerender, container } = render(
|
||||
<ImageAttachment
|
||||
images={uniqueImages}
|
||||
onAdd={mockOnAdd}
|
||||
onRemove={mockOnRemove}
|
||||
onPreview={mockOnPreview}
|
||||
/>
|
||||
);
|
||||
|
||||
// Verify initial state
|
||||
const initialCount = uniqueImages.length;
|
||||
const countElement = container.querySelector('.image-attachment__count');
|
||||
expect(countElement?.textContent?.trim().replace(/\s+/g, ' ')).toBe(`${initialCount} / 9`);
|
||||
|
||||
// Find and click delete button for the target image
|
||||
const deleteButtons = container.querySelectorAll('[aria-label="删除图片"]');
|
||||
fireEvent.click(deleteButtons[deleteIndex]);
|
||||
|
||||
// Verify onRemove was called with correct ID
|
||||
expect(deletedImageId).toBe(imageToDelete.id);
|
||||
|
||||
// Simulate state update after deletion
|
||||
const updatedImages = uniqueImages.filter((img) => img.id !== imageToDelete.id);
|
||||
|
||||
rerender(
|
||||
<ImageAttachment
|
||||
images={updatedImages}
|
||||
onAdd={mockOnAdd}
|
||||
onRemove={mockOnRemove}
|
||||
onPreview={mockOnPreview}
|
||||
/>
|
||||
);
|
||||
|
||||
// Property verification:
|
||||
// 1. List length should decrease by 1
|
||||
const newCount = updatedImages.length;
|
||||
expect(newCount).toBe(initialCount - 1);
|
||||
|
||||
const newCountElement = container.querySelector('.image-attachment__count');
|
||||
expect(newCountElement?.textContent?.trim().replace(/\s+/g, ' ')).toBe(`${newCount} / 9`);
|
||||
|
||||
// 2. Deleted image should not be in the list
|
||||
const remainingImages = container.querySelectorAll('img');
|
||||
expect(remainingImages).toHaveLength(newCount);
|
||||
|
||||
// 3. All remaining images should be different from deleted image
|
||||
const remainingIds = updatedImages.map((img) => img.id);
|
||||
expect(remainingIds).not.toContain(imageToDelete.id);
|
||||
|
||||
// Clean up after iteration
|
||||
cleanup();
|
||||
|
||||
return true;
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Property: Image count display consistency
|
||||
* For any valid image array (0-9 images), the displayed count should match the array length
|
||||
*
|
||||
* **Validates: Requirements 4.9**
|
||||
*/
|
||||
it('Property: should display correct image count for any valid array', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(
|
||||
fc.record({
|
||||
id: fc.integer({ min: 1, max: 10000 }),
|
||||
transactionId: fc.constant(1),
|
||||
filePath: fc.string(),
|
||||
fileName: fc.string(),
|
||||
fileSize: fc.integer({ min: 1, max: 10485760 }),
|
||||
mimeType: fc.constantFrom('image/jpeg', 'image/png', 'image/heic'),
|
||||
createdAt: fc.constant('2024-01-01T00:00:00Z'),
|
||||
}),
|
||||
{ minLength: 0, maxLength: 9 }
|
||||
),
|
||||
(images) => {
|
||||
// Clean up before each iteration
|
||||
cleanup();
|
||||
|
||||
// Ensure unique IDs
|
||||
const uniqueImages = images.map((img, idx) => ({
|
||||
...img,
|
||||
id: idx + 1,
|
||||
}));
|
||||
|
||||
const mockOnAdd = vi.fn();
|
||||
const mockOnRemove = vi.fn();
|
||||
const mockOnPreview = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
<ImageAttachment
|
||||
images={uniqueImages}
|
||||
onAdd={mockOnAdd}
|
||||
onRemove={mockOnRemove}
|
||||
onPreview={mockOnPreview}
|
||||
/>
|
||||
);
|
||||
|
||||
// Verify count display
|
||||
const expectedCount = uniqueImages.length;
|
||||
const countElement = container.querySelector('.image-attachment__count');
|
||||
expect(countElement?.textContent?.trim().replace(/\s+/g, ' ')).toBe(`${expectedCount} / 9`);
|
||||
|
||||
// Verify actual rendered images match count
|
||||
const renderedImages = container.querySelectorAll('img');
|
||||
expect(renderedImages).toHaveLength(expectedCount);
|
||||
|
||||
// Clean up after iteration
|
||||
cleanup();
|
||||
|
||||
return true;
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Property: Add button visibility based on image count
|
||||
* For any image array, add button should be visible if count < 9, hidden if count >= 9
|
||||
*
|
||||
* **Validates: Requirements 4.9, 4.12**
|
||||
*/
|
||||
it('Property: should show/hide add button based on image count', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(
|
||||
fc.record({
|
||||
id: fc.integer({ min: 1, max: 10000 }),
|
||||
transactionId: fc.constant(1),
|
||||
filePath: fc.string(),
|
||||
fileName: fc.string(),
|
||||
fileSize: fc.integer({ min: 1, max: 10485760 }),
|
||||
mimeType: fc.constantFrom('image/jpeg', 'image/png', 'image/heic'),
|
||||
createdAt: fc.constant('2024-01-01T00:00:00Z'),
|
||||
}),
|
||||
{ minLength: 0, maxLength: 9 }
|
||||
),
|
||||
(images) => {
|
||||
// Clean up before each iteration
|
||||
cleanup();
|
||||
|
||||
// Ensure unique IDs
|
||||
const uniqueImages = images.map((img, idx) => ({
|
||||
...img,
|
||||
id: idx + 1,
|
||||
}));
|
||||
|
||||
const mockOnAdd = vi.fn();
|
||||
const mockOnRemove = vi.fn();
|
||||
const mockOnPreview = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
<ImageAttachment
|
||||
images={uniqueImages}
|
||||
onAdd={mockOnAdd}
|
||||
onRemove={mockOnRemove}
|
||||
onPreview={mockOnPreview}
|
||||
/>
|
||||
);
|
||||
|
||||
const addButton = container.querySelector('[aria-label="添加图片"]');
|
||||
const imageCount = uniqueImages.length;
|
||||
|
||||
// Property: Add button visible iff count < 9
|
||||
if (imageCount < 9) {
|
||||
expect(addButton).toBeInTheDocument();
|
||||
} else {
|
||||
expect(addButton).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
// Clean up after iteration
|
||||
cleanup();
|
||||
|
||||
return true;
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Property: Warning display based on image count
|
||||
* For any image array, warning should be visible if count >= 7, hidden if count < 7
|
||||
*
|
||||
* **Validates: Requirements 4.9, 4.12**
|
||||
*/
|
||||
it('Property: should show warning when approaching limit', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(
|
||||
fc.record({
|
||||
id: fc.integer({ min: 1, max: 10000 }),
|
||||
transactionId: fc.constant(1),
|
||||
filePath: fc.string(),
|
||||
fileName: fc.string(),
|
||||
fileSize: fc.integer({ min: 1, max: 10485760 }),
|
||||
mimeType: fc.constantFrom('image/jpeg', 'image/png', 'image/heic'),
|
||||
createdAt: fc.constant('2024-01-01T00:00:00Z'),
|
||||
}),
|
||||
{ minLength: 0, maxLength: 9 }
|
||||
),
|
||||
(images) => {
|
||||
// Clean up before each iteration
|
||||
cleanup();
|
||||
|
||||
// Ensure unique IDs
|
||||
const uniqueImages = images.map((img, idx) => ({
|
||||
...img,
|
||||
id: idx + 1,
|
||||
}));
|
||||
|
||||
const mockOnAdd = vi.fn();
|
||||
const mockOnRemove = vi.fn();
|
||||
const mockOnPreview = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
<ImageAttachment
|
||||
images={uniqueImages}
|
||||
onAdd={mockOnAdd}
|
||||
onRemove={mockOnRemove}
|
||||
onPreview={mockOnPreview}
|
||||
/>
|
||||
);
|
||||
|
||||
const warning = container.querySelector('.image-attachment__warning');
|
||||
const imageCount = uniqueImages.length;
|
||||
|
||||
// Property: Warning visible iff count >= 7
|
||||
if (imageCount >= 7) {
|
||||
expect(warning).toBeInTheDocument();
|
||||
} else {
|
||||
expect(warning).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
// Clean up after iteration
|
||||
cleanup();
|
||||
|
||||
return true;
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Property: Delete button count matches image count
|
||||
* For any non-empty image array (when not disabled),
|
||||
* the number of delete buttons should equal the number of images
|
||||
*
|
||||
* **Validates: Requirements 4.7**
|
||||
*/
|
||||
it('Property: should render delete button for each image', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(
|
||||
fc.record({
|
||||
id: fc.integer({ min: 1, max: 10000 }),
|
||||
transactionId: fc.constant(1),
|
||||
filePath: fc.string(),
|
||||
fileName: fc.string(),
|
||||
fileSize: fc.integer({ min: 1, max: 10485760 }),
|
||||
mimeType: fc.constantFrom('image/jpeg', 'image/png', 'image/heic'),
|
||||
createdAt: fc.constant('2024-01-01T00:00:00Z'),
|
||||
}),
|
||||
{ minLength: 1, maxLength: 9 }
|
||||
),
|
||||
(images) => {
|
||||
// Clean up before each iteration
|
||||
cleanup();
|
||||
|
||||
// Ensure unique IDs
|
||||
const uniqueImages = images.map((img, idx) => ({
|
||||
...img,
|
||||
id: idx + 1,
|
||||
}));
|
||||
|
||||
const mockOnAdd = vi.fn();
|
||||
const mockOnRemove = vi.fn();
|
||||
const mockOnPreview = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
<ImageAttachment
|
||||
images={uniqueImages}
|
||||
onAdd={mockOnAdd}
|
||||
onRemove={mockOnRemove}
|
||||
onPreview={mockOnPreview}
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteButtons = container.querySelectorAll('[aria-label="删除图片"]');
|
||||
|
||||
// Property: Number of delete buttons equals number of images
|
||||
expect(deleteButtons).toHaveLength(uniqueImages.length);
|
||||
|
||||
// Clean up after iteration
|
||||
cleanup();
|
||||
|
||||
return true;
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Property: Preview callback receives correct index
|
||||
* For any image array and any click on an image,
|
||||
* the onPreview callback should be called with the correct index
|
||||
*
|
||||
* **Validates: Requirements 4.6**
|
||||
*/
|
||||
it('Property: should call onPreview with correct index', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.array(
|
||||
fc.record({
|
||||
id: fc.integer({ min: 1, max: 10000 }),
|
||||
transactionId: fc.constant(1),
|
||||
filePath: fc.string(),
|
||||
fileName: fc.string(),
|
||||
fileSize: fc.integer({ min: 1, max: 10485760 }),
|
||||
mimeType: fc.constantFrom('image/jpeg', 'image/png', 'image/heic'),
|
||||
createdAt: fc.constant('2024-01-01T00:00:00Z'),
|
||||
}),
|
||||
{ minLength: 1, maxLength: 9 }
|
||||
),
|
||||
fc.nat(),
|
||||
(images, clickIndexRaw) => {
|
||||
// Clean up before each iteration
|
||||
cleanup();
|
||||
|
||||
// Ensure unique IDs
|
||||
const uniqueImages = images.map((img, idx) => ({
|
||||
...img,
|
||||
id: idx + 1,
|
||||
}));
|
||||
|
||||
const clickIndex = clickIndexRaw % uniqueImages.length;
|
||||
|
||||
let previewedIndex: number | null = null;
|
||||
const mockOnPreview = (index: number) => {
|
||||
previewedIndex = index;
|
||||
};
|
||||
|
||||
const mockOnAdd = vi.fn();
|
||||
const mockOnRemove = vi.fn();
|
||||
|
||||
const { container } = render(
|
||||
<ImageAttachment
|
||||
images={uniqueImages}
|
||||
onAdd={mockOnAdd}
|
||||
onRemove={mockOnRemove}
|
||||
onPreview={mockOnPreview}
|
||||
/>
|
||||
);
|
||||
|
||||
// Click on the image at clickIndex
|
||||
const imageItems = container.querySelectorAll('.image-attachment__item');
|
||||
fireEvent.click(imageItems[clickIndex]);
|
||||
|
||||
// Property: onPreview called with correct index
|
||||
expect(previewedIndex).toBe(clickIndex);
|
||||
|
||||
// Clean up after iteration
|
||||
cleanup();
|
||||
|
||||
return true;
|
||||
}
|
||||
),
|
||||
{ numRuns: 100 }
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user