Datasourceforcryptocurrency / tests /test_ui_feedback.test.js
Really-amin's picture
Upload 303 files
b068b76 verified
/**
* Property-Based Tests for ui-feedback.js
*
* Feature: frontend-cleanup, Property 4: Error toast display
* Validates: Requirements 3.4, 4.4, 7.3
*
* Property 4: Error toast display
* For any failed API call, the UIFeedback.fetchJSON function should display
* an error toast with the error message
*/
import fc from 'fast-check';
import { JSDOM } from 'jsdom';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Load ui-feedback.js content
const uiFeedbackPath = path.join(__dirname, '..', 'static', 'js', 'ui-feedback.js');
const uiFeedbackCode = fs.readFileSync(uiFeedbackPath, 'utf-8');
// Helper to create a fresh DOM environment for each test
async function createTestEnvironment() {
const html = `
<!DOCTYPE html>
<html>
<head></head>
<body>
<script>${uiFeedbackCode}</script>
</body>
</html>
`;
const dom = new JSDOM(html, {
url: 'http://localhost',
runScripts: 'dangerously',
resources: 'usable'
});
const { window } = dom;
const { document } = window;
// Wait for scripts to execute and DOMContentLoaded to fire
await new Promise(resolve => {
if (document.readyState === 'complete') {
resolve();
} else {
window.addEventListener('load', resolve);
}
});
// Give a bit more time for the toast stack to be appended
await new Promise(resolve => setTimeout(resolve, 50));
return { window, document };
}
// Mock fetch to simulate API failures
function createMockFetch(shouldFail, statusCode, errorMessage) {
return async (url, options) => {
if (shouldFail) {
if (statusCode) {
// HTTP error response
return {
ok: false,
status: statusCode,
statusText: errorMessage || 'Error',
text: async () => errorMessage || 'Request failed',
json: async () => { throw new Error('Invalid JSON'); }
};
} else {
// Network error
throw new Error(errorMessage || 'Network error');
}
}
// Success case
return {
ok: true,
status: 200,
json: async () => ({ data: 'success' })
};
};
}
console.log('Running Property-Based Tests for ui-feedback.js...\n');
async function runTests() {
console.log('Property 4.1: fetchJSON should display error toast on HTTP errors');
// Test that HTTP errors (4xx, 5xx) trigger error toasts
await fc.assert(
fc.asyncProperty(
fc.integer({ min: 400, max: 599 }), // HTTP error status codes
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // Non-empty error message
async (statusCode, errorMessage) => {
const { window, document } = await createTestEnvironment();
// Mock fetch to return HTTP error
window.fetch = createMockFetch(true, statusCode, errorMessage);
// Track toast creation
let toastCreated = false;
let toastType = null;
let toastContent = null;
// Check if UIFeedback is defined
if (!window.UIFeedback) {
throw new Error('UIFeedback not defined on window');
}
// Override toast creation to capture calls
const originalToast = window.UIFeedback.toast;
window.UIFeedback.toast = (type, title, message) => {
toastCreated = true;
toastType = type;
toastContent = { title, message };
// Still create the actual toast
originalToast(type, title, message);
};
// Call fetchJSON and expect it to throw
let errorThrown = false;
try {
await window.UIFeedback.fetchJSON('/api/test', {}, 'Test Context');
} catch (err) {
errorThrown = true;
}
// Verify error toast was created
if (!toastCreated) {
throw new Error(`No toast created for HTTP ${statusCode} error`);
}
if (toastType !== 'error') {
throw new Error(`Expected error toast, got ${toastType}`);
}
if (!errorThrown) {
throw new Error('fetchJSON should throw error on HTTP failure');
}
// Verify toast is in the DOM
const toastStack = document.querySelector('.toast-stack');
if (!toastStack) {
throw new Error('Toast stack not found in DOM');
}
const errorToasts = toastStack.querySelectorAll('.toast.error');
if (errorToasts.length === 0) {
throw new Error('No error toast found in toast stack');
}
return true;
}
),
{ numRuns: 50, verbose: true }
);
console.log('βœ“ Property 4.1 passed: HTTP errors trigger error toasts\n');
console.log('Property 4.2: fetchJSON should display error toast on network errors');
// Test that network errors trigger error toasts
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), // Non-empty error message
async (errorMessage) => {
const { window, document } = await createTestEnvironment();
// Mock fetch to throw network error
window.fetch = createMockFetch(true, null, errorMessage);
// Track toast creation
let toastCreated = false;
let toastType = null;
// Override toast creation to capture calls
const originalToast = window.UIFeedback.toast;
window.UIFeedback.toast = (type, title, message) => {
toastCreated = true;
toastType = type;
originalToast(type, title, message);
};
// Call fetchJSON and expect it to throw
let errorThrown = false;
try {
await window.UIFeedback.fetchJSON('/api/test', {}, 'Test Context');
} catch (err) {
errorThrown = true;
}
// Verify error toast was created
if (!toastCreated) {
throw new Error('No toast created for network error');
}
if (toastType !== 'error') {
throw new Error(`Expected error toast, got ${toastType}`);
}
if (!errorThrown) {
throw new Error('fetchJSON should throw error on network failure');
}
return true;
}
),
{ numRuns: 50, verbose: true }
);
console.log('βœ“ Property 4.2 passed: Network errors trigger error toasts\n');
console.log('Property 4.3: fetchJSON should return data on success');
// Test that successful requests don't create error toasts
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // URL path
async (urlPath) => {
const { window } = await createTestEnvironment();
// Mock fetch to return success
const mockData = { result: 'success', path: urlPath };
window.fetch = async () => ({
ok: true,
status: 200,
json: async () => mockData
});
// Track toast creation
let errorToastCreated = false;
// Override toast creation to capture calls
const originalToast = window.UIFeedback.toast;
window.UIFeedback.toast = (type, title, message) => {
if (type === 'error') {
errorToastCreated = true;
}
originalToast(type, title, message);
};
// Call fetchJSON
const result = await window.UIFeedback.fetchJSON(`/api/${urlPath}`, {}, 'Test');
// Verify no error toast was created
if (errorToastCreated) {
throw new Error('Error toast created for successful request');
}
// Verify data was returned
if (JSON.stringify(result) !== JSON.stringify(mockData)) {
throw new Error('fetchJSON did not return correct data');
}
return true;
}
),
{ numRuns: 50, verbose: true }
);
console.log('βœ“ Property 4.3 passed: Successful requests return data without error toasts\n');
console.log('Property 4.4: toast function should create visible toast elements');
// Test that toast function creates DOM elements
await fc.assert(
fc.asyncProperty(
fc.constantFrom('success', 'error', 'warning', 'info'), // Toast types
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), // Title
fc.option(fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0), { nil: null }), // Optional message
async (type, title, message) => {
const { window, document } = await createTestEnvironment();
// Create toast
window.UIFeedback.toast(type, title, message);
// Verify toast was added to DOM
const toastStack = document.querySelector('.toast-stack');
if (!toastStack) {
throw new Error('Toast stack not found');
}
const toasts = toastStack.querySelectorAll(`.toast.${type}`);
if (toasts.length === 0) {
throw new Error(`No ${type} toast found in stack`);
}
const lastToast = toasts[toasts.length - 1];
const toastHTML = lastToast.innerHTML;
// Verify title is in toast
if (!toastHTML.includes(title)) {
throw new Error(`Toast does not contain title: ${title}`);
}
// Verify message is in toast if provided
if (message && !toastHTML.includes(message)) {
throw new Error(`Toast does not contain message: ${message}`);
}
return true;
}
),
{ numRuns: 50, verbose: true }
);
console.log('βœ“ Property 4.4 passed: Toast function creates visible elements\n');
console.log('Property 4.5: setBadge should update element class and text');
// Test that setBadge updates badge elements correctly
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 1, maxLength: 30 }).filter(s => s.trim().length > 0), // Badge text
fc.constantFrom('info', 'success', 'warning', 'danger'), // Badge tone
async (text, tone) => {
const { window, document } = await createTestEnvironment();
// Create a badge element
const badge = document.createElement('span');
badge.className = 'badge';
document.body.appendChild(badge);
// Update badge
window.UIFeedback.setBadge(badge, text, tone);
// Verify text was set
if (badge.textContent !== text) {
throw new Error(`Badge text not set correctly. Expected: ${text}, Got: ${badge.textContent}`);
}
// Verify class was set
if (!badge.classList.contains('badge')) {
throw new Error('Badge should have "badge" class');
}
if (!badge.classList.contains(tone)) {
throw new Error(`Badge should have "${tone}" class`);
}
return true;
}
),
{ numRuns: 50, verbose: true }
);
console.log('βœ“ Property 4.5 passed: setBadge updates element correctly\n');
console.log('Property 4.6: showLoading should display loading indicator');
// Test that showLoading creates loading indicators
await fc.assert(
fc.asyncProperty(
fc.option(fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0), { nil: undefined }), // Optional message
async (message) => {
const { window, document } = await createTestEnvironment();
// Create a container
const container = document.createElement('div');
container.id = 'test-container';
document.body.appendChild(container);
// Show loading
window.UIFeedback.showLoading(container, message);
// Verify loading indicator was added
const loadingIndicator = container.querySelector('.loading-indicator');
if (!loadingIndicator) {
throw new Error('Loading indicator not found');
}
// Verify message is displayed
const expectedMessage = message || 'Loading data...';
if (!loadingIndicator.textContent.includes(expectedMessage)) {
throw new Error(`Loading indicator does not contain expected message: ${expectedMessage}`);
}
return true;
}
),
{ numRuns: 50, verbose: true }
);
console.log('βœ“ Property 4.6 passed: showLoading displays loading indicator\n');
console.log('Property 4.7: fadeReplace should update container content');
// Test that fadeReplace updates content
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 1, maxLength: 200 }).filter(s => s.trim().length > 0), // HTML content
async (html) => {
const { window, document } = await createTestEnvironment();
// Create a container
const container = document.createElement('div');
container.id = 'test-container';
container.innerHTML = '<p>Old content</p>';
document.body.appendChild(container);
// Replace content
window.UIFeedback.fadeReplace(container, html);
// Verify content was replaced
if (container.innerHTML !== html) {
throw new Error('Container content not replaced');
}
// Verify fade-in class was added (may be removed by timeout)
// We just check that the content was updated
return true;
}
),
{ numRuns: 50, verbose: true }
);
console.log('βœ“ Property 4.7 passed: fadeReplace updates container content\n');
console.log('\nβœ“ All property-based tests for ui-feedback.js passed!');
console.log('βœ“ Property 4: Error toast display validated successfully');
}
runTests().catch(err => {
console.error('Test failed:', err);
process.exit(1);
});