diff --git a/public/calculators/subnet.js b/public/calculators/subnet.js deleted file mode 100644 index 8986937..0000000 --- a/public/calculators/subnet.js +++ /dev/null @@ -1,895 +0,0 @@ -import {revive, persist, labelInput, labelSelect} from '/js/util.js'; - -export default { - id:'subnet', name:'IP Subnet Calculator', about:'Calculate IPv4 and IPv6 subnet information, CIDR notation, and network ranges.', - render(root){ - const key='calc_subnet_v1'; - const s = revive(key,{ - ipVersion: 'ipv4', - ipAddress: '192.168.1.0', - subnetMask: '255.255.255.0', - cidr: 24, - customCidr: 24 - }); - - const ui = document.createElement('div'); - - // IP version selector - const versionSection = document.createElement('div'); - versionSection.innerHTML = ` -

IP Version

-
- -
- `; - - // IPv4 input section - const ipv4Section = document.createElement('div'); - ipv4Section.innerHTML = ` -
-

IPv4 Configuration

-
-
- - -
-
- - -
-
-
- - - / -
-
- `; - - // IPv6 input section - const ipv6Section = document.createElement('div'); - ipv6Section.id = 'ipv6-inputs'; - ipv6Section.style.display = 'none'; - - // IPv6 title - const ipv6Title = document.createElement('h3'); - ipv6Title.style.cssText = 'color: var(--accent); margin-bottom: 15px;'; - ipv6Title.textContent = 'IPv6 Configuration'; - ipv6Section.appendChild(ipv6Title); - - // IPv6 Address input container - const ipv6AddressContainer = document.createElement('div'); - ipv6AddressContainer.style.cssText = 'margin-bottom: 20px;'; - - const ipv6AddressLabel = document.createElement('label'); - ipv6AddressLabel.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);'; - ipv6AddressLabel.textContent = 'IPv6 Address'; - - const ipv6AddressInput = document.createElement('input'); - ipv6AddressInput.type = 'text'; - ipv6AddressInput.name = 'ipv6Address'; - ipv6AddressInput.value = '2001:db8::'; - ipv6AddressInput.placeholder = '2001:db8::'; - ipv6AddressInput.style.cssText = 'width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 16px;'; - - ipv6AddressContainer.appendChild(ipv6AddressLabel); - ipv6AddressContainer.appendChild(ipv6AddressInput); - - // IPv6 CIDR input container - const ipv6CidrContainer = document.createElement('div'); - ipv6CidrContainer.style.cssText = 'margin-bottom: 20px;'; - - const ipv6CidrLabel = document.createElement('label'); - ipv6CidrLabel.style.cssText = 'display: block; margin-bottom: 8px; font-weight: 500; color: var(--text);'; - ipv6CidrLabel.textContent = 'CIDR Prefix Length'; - - const ipv6CidrInput = document.createElement('input'); - ipv6CidrInput.type = 'number'; - ipv6CidrInput.name = 'ipv6Cidr'; - ipv6CidrInput.value = '64'; - ipv6CidrInput.min = '0'; - ipv6CidrInput.max = '128'; - ipv6CidrInput.style.cssText = 'width: 200px; padding: 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 16px;'; - - const ipv6CidrSpan = document.createElement('span'); - ipv6CidrSpan.style.cssText = 'color: var(--muted); margin-left: 10px;'; - ipv6CidrSpan.textContent = '/'; - - ipv6CidrContainer.appendChild(ipv6CidrLabel); - ipv6CidrContainer.appendChild(ipv6CidrInput); - ipv6CidrContainer.appendChild(ipv6CidrSpan); - - // Add all elements to IPv6 section - ipv6Section.appendChild(ipv6AddressContainer); - ipv6Section.appendChild(ipv6CidrContainer); - - ui.append(versionSection, ipv4Section, ipv6Section); - - // Results section - const out = document.createElement('div'); - out.className = 'result'; - out.style.cssText = ` - margin: 20px 0; - padding: 15px; - background: var(--k-bg); - border-radius: 8px; - border-left: 4px solid var(--accent); - `; - ui.append(out); - - // Utility functions - function getNetworkClass(ip) { - const firstOctet = parseInt(ip.split('.')[0]); - if (firstOctet >= 0 && firstOctet <= 127) return 'A'; - if (firstOctet >= 128 && firstOctet <= 191) return 'B'; - if (firstOctet >= 192 && firstOctet <= 223) return 'C'; - if (firstOctet >= 224 && firstOctet <= 239) return 'D'; - if (firstOctet >= 240 && firstOctet <= 255) return 'E'; - return 'Unknown'; - } - - function ipToLong(ip) { - return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet), 0) >>> 0; - } - - function longToIp(long) { - return [ - (long >>> 24) & 255, - (long >>> 16) & 255, - (long >>> 8) & 255, - long & 255 - ].join('.'); - } - - function cidrToMask(cidr) { - // Handle edge cases - if (cidr === 0) return '0.0.0.0'; - if (cidr === 32) return '255.255.255.255'; - - // For other CIDR values, use the bit manipulation approach - // But handle the 32-bit overflow issue - let mask; - if (cidr === 31) { - // Special case for /31 to avoid overflow - mask = 0xFFFFFFFE; - } else { - // Calculate mask: create a number with 'cidr' leading 1s - mask = 0; - for (let i = 31; i >= (32 - cidr); i--) { - mask |= (1 << i); - } - } - - return longToIp(mask); - } - - function maskToCidr(mask) { - const maskLong = ipToLong(mask); - - // Handle edge cases - if (maskLong === 0) return 0; // 0.0.0.0 = /0 - if (maskLong === 0xFFFFFFFF) return 32; // 255.255.255.255 = /32 - - // Count leading 1s in binary representation - let cidr = 0; - let temp = maskLong; - - // Count consecutive 1s from left to right - for (let i = 31; i >= 0; i--) { - if ((temp & (1 << i)) !== 0) { - cidr++; - } else { - break; // Stop at first 0 - } - } - - return cidr; - } - - function validateIPv4(ip) { - // Handle CIDR notation by extracting just the IP part - const ipPart = ip.includes('/') ? ip.split('/')[0] : ip; - - const parts = ipPart.split('.'); - if (parts.length !== 4) return false; - return parts.every(part => { - const num = parseInt(part); - return num >= 0 && num <= 255 && part === num.toString(); - }); - } - - function validateSubnetMask(mask) { - if (!validateIPv4(mask)) return false; - - // Check that it's a valid subnet mask (consecutive 1s followed by 0s) - const maskLong = ipToLong(mask); - const binary = maskLong.toString(2).padStart(32, '0'); - - // Find the first 0 - const firstZero = binary.indexOf('0'); - - // Special case: 255.255.255.255 (/32) is valid - if (firstZero === -1) return true; // All 1s (255.255.255.255) is valid for /32 - - // Check that all bits after first 0 are also 0 - return binary.substring(firstZero).indexOf('1') === -1; - } - - function validateIPv6(ip) { - // Proper IPv6 validation - if (!ip || typeof ip !== 'string') return false; - - // Check for basic IPv6 format (contains colons) - if (!ip.includes(':')) return false; - - // Split by double colon and validate each part - const parts = ip.split('::'); - if (parts.length > 2) return false; // Only one double colon allowed - - // Validate each part - for (let part of parts) { - if (part === '') continue; // Empty part is allowed for :: notation - - const segments = part.split(':'); - for (let segment of segments) { - if (segment === '') continue; // Empty segment is allowed - - // Each segment should be 1-4 hex characters - if (!/^[0-9a-fA-F]{1,4}$/.test(segment)) { - return false; - } - } - } - - return true; - } - - function expandIPv6(ip) { - // Expand compressed IPv6 address - const parts = ip.split('::'); - if (parts.length === 1) return ip; - - const left = parts[0].split(':'); - const right = parts[1] ? parts[1].split(':') : []; - const missing = 8 - left.length - right.length; - - const expanded = [...left]; - for (let i = 0; i < missing; i++) { - expanded.push('0000'); - } - expanded.push(...right); - - return expanded.join(':'); - } - - function compressIPv6(ip) { - // Compress IPv6 address to shortest possible form - const parts = ip.split(':'); - - // Find the longest sequence of zeros - let longestZeroStart = -1; - let longestZeroLength = 0; - let currentZeroStart = -1; - let currentZeroLength = 0; - - for (let i = 0; i < parts.length; i++) { - if (parts[i] === '0000' || parts[i] === '0') { - if (currentZeroStart === -1) { - currentZeroStart = i; - currentZeroLength = 1; - } else { - currentZeroLength++; - } - } else { - if (currentZeroLength > longestZeroLength) { - longestZeroStart = currentZeroStart; - longestZeroLength = currentZeroLength; - } - currentZeroStart = -1; - currentZeroLength = 0; - } - } - - // Check if the last sequence is the longest - if (currentZeroLength > longestZeroLength) { - longestZeroStart = currentZeroStart; - longestZeroLength = currentZeroLength; - } - - // Only compress if we have at least 2 consecutive zeros - if (longestZeroLength >= 2) { - const left = parts.slice(0, longestZeroStart); - const right = parts.slice(longestZeroStart + longestZeroLength); - - // Remove leading zeros from each part - const leftCompressed = left.map(part => { - const num = parseInt(part, 16); - return num.toString(16); - }); - const rightCompressed = right.map(part => { - const num = parseInt(part, 16); - return num.toString(16); - }); - - // Handle edge cases for proper :: placement - if (leftCompressed.length === 0 && rightCompressed.length === 0) { - return '::'; - } else if (leftCompressed.length === 0) { - return '::' + rightCompressed.join(':'); - } else if (rightCompressed.length === 0) { - return leftCompressed.join(':') + '::'; - } else { - return [...leftCompressed, '', ...rightCompressed].join(':'); - } - } else { - // No compression needed, just remove leading zeros - return parts.map(part => { - const num = parseInt(part, 16); - return num.toString(16); - }).join(':'); - } - } - - function ipv6ToLong(ip) { - const expanded = expandIPv6(ip); - const parts = expanded.split(':'); - let result = 0n; - - // IPv6 addresses are big-endian (most significant byte first) - // Each part is a 16-bit hex value - for (let i = 0; i < 8; i++) { - const part = parseInt(parts[i], 16); - result = (result << 16n) + BigInt(part); - } - - return result; - } - - function longToIPv6(long) { - const parts = []; - // Extract each 16-bit segment in big-endian order - // Start from the most significant bits (left side) - for (let i = 7; i >= 0; i--) { - const part = Number((long >> BigInt(i * 16)) & 0xFFFFn); - parts.push(part.toString(16).padStart(4, '0')); - } - return parts.join(':'); - } - - function generateAvailableNetworks(baseIP, cidr) { - const networks = []; - const networkSize = Math.pow(2, 32 - cidr); - const baseLong = ipToLong(baseIP); - - // Calculate the base network for the IP (up to /16 level) - // For example: 192.168.1.0 -> 192.168.0.0 (base /16 network) - const baseNetworkLong = baseLong & ((0xFFFFFFFF << (32 - 16)) >>> 0); - const networkLong = baseNetworkLong; - - // Show up to 64 networks - const count = Math.min(64, Math.floor(65536 / networkSize)); - - for (let i = 0; i < count; i++) { - const networkAddr = networkLong + (i * networkSize); - const broadcastAddr = networkAddr + networkSize - 1; - const firstHost = networkAddr + 1; - const lastHost = broadcastAddr - 1; - - networks.push({ - network: longToIp(networkAddr), - firstHost: longToIp(firstHost), - lastHost: longToIp(lastHost), - broadcast: longToIp(broadcastAddr) - }); - } - - return networks; - } - - function calculateIPv4() { - const ipAddress = ui.querySelector('[name=ipAddress]').value; - const subnetMask = ui.querySelector('[name=subnetMask]').value; - const cidr = +ui.querySelector('[name=cidr]').value; - - if (!validateIPv4(ipAddress)) { - out.innerHTML = `
- Error: Invalid IPv4 address format -
`; - return; - } - - if (!validateSubnetMask(subnetMask)) { - out.innerHTML = `
- Error: Invalid subnet mask format -
`; - return; - } - - const ipLong = ipToLong(ipAddress); - const maskLong = ipToLong(subnetMask); - const networkLong = (ipLong & maskLong) >>> 0; // Ensure unsigned 32-bit - const broadcastLong = (networkLong | (~maskLong >>> 0)) >>> 0; // Ensure unsigned 32-bit - - // Calculate CIDR from subnet mask - const calculatedCidr = maskToCidr(subnetMask); - - // Handle edge cases for host calculations - let totalHosts, firstHostLong, lastHostLong; - - if (calculatedCidr === 32) { - // /32 - single host, no usable hosts - totalHosts = 1; - firstHostLong = networkLong; // Same as network - lastHostLong = networkLong; // Same as network - } else if (calculatedCidr === 31) { - // /31 - point-to-point, no usable hosts - totalHosts = 2; - firstHostLong = networkLong; // First address - lastHostLong = broadcastLong; // Second address - } else if (calculatedCidr === 30) { - // /30 - 4 total hosts, 2 usable - totalHosts = 4; - firstHostLong = networkLong + 1; - lastHostLong = broadcastLong - 1; - } else { - // Normal case - calculate usable hosts - totalHosts = Math.pow(2, 32 - calculatedCidr) - 2; - firstHostLong = networkLong + 1; - lastHostLong = broadcastLong - 1; - } - - // Calculate total possible networks - const networkSize = Math.pow(2, 32 - calculatedCidr); - const totalPossibleNetworks = Math.floor(65536 / networkSize); - - // Generate available networks table - const availableNetworks = generateAvailableNetworks(ipAddress, calculatedCidr); - - out.innerHTML = ` -
- IPv4 Subnet Information -
- -
-
-

Input Information

-
- IP Address: ${ipAddress} -
-
- Network Class: Class ${getNetworkClass(ipAddress)} -
-
- Subnet Mask: ${subnetMask} -
-
- CIDR Notation: /${calculatedCidr.toFixed(0)} -
-
- -
-

Network Information

-
- Network Address: ${longToIp(networkLong)} -
-
- Broadcast Address: ${longToIp(broadcastLong)} -
-
- Total Hosts: ${totalHosts.toLocaleString()} -
-
-
- -
-

Host Information

-
- First Usable Host: ${longToIp(firstHostLong)} -
-
- Last Usable Host: ${longToIp(lastHostLong)} -
-
- -
-

Binary Representation

-
- IP Address: ${ipLong.toString(2).padStart(32, '0').match(/.{1,8}/g).join('.')} - (0x${ipLong.toString(16).padStart(8, '0').toUpperCase()}) -
-
- Subnet Mask: ${maskLong.toString(2).padStart(32, '0').match(/.{1,8}/g).join('.')} - (0x${maskLong.toString(16).padStart(8, '0').toUpperCase()}) -
-
- Network: ${networkLong.toString(2).padStart(32, '0').match(/.{1,8}/g).join('.')} - (0x${networkLong.toString(16).padStart(8, '0').toUpperCase()}) -
-
- -
-

Available Networks

-
- - - - - - - - - - - ${availableNetworks.map(net => ` - - - - - - - `).join('')} - -
NetworkFirst HostLast HostBroadcast
${net.network}${net.firstHost}${net.lastHost}${net.broadcast}
-
-
- Showing ${availableNetworks.length} of ${totalPossibleNetworks} possible networks -
-
- `; - } - - function calculateIPv6() { - const ipv6Address = ui.querySelector('[name=ipv6Address]').value; - const ipv6Cidr = +ui.querySelector('[name=ipv6Cidr]').value; - - if (!validateIPv6(ipv6Address)) { - out.innerHTML = `
- Error: Invalid IPv6 address format -
`; - return; - } - - if (ipv6Cidr < 0 || ipv6Cidr > 128) { - out.innerHTML = `
- Error: CIDR must be between 0 and 128 -
`; - return; - } - - // Expand the IPv6 address - const expandedIPv6 = expandIPv6(ipv6Address); - - // Calculate network and broadcast addresses - const ipv6Long = ipv6ToLong(expandedIPv6); - - // For IPv6, we need to handle the network portion correctly - const networkBits = BigInt(ipv6Cidr); - const hostBits = BigInt(128 - ipv6Cidr); - - // Create network mask: 1s for network bits, 0s for host bits - // For IPv6, we need to create a mask with networkBits 1s followed by hostBits 0s - let networkMask = 0n; - for (let i = 127n; i >= hostBits; i--) { - networkMask |= (1n << i); - } - - // Calculate network address: clear host bits (keep network bits) - const networkLong = ipv6Long & networkMask; - - // Calculate broadcast address: set host bits to 1 (keep network bits, set host bits) - const broadcastLong = networkLong | ((1n << hostBits) - 1n); - - // Calculate number of hosts (subtract 2 for network and broadcast) - const totalHosts = (BigInt(2) ** BigInt(128 - ipv6Cidr)) - BigInt(2); - - // Calculate total possible subnets in a /64 (typical IPv6 subnet size) - const totalPossibleSubnets = BigInt(2) ** BigInt(64 - ipv6Cidr); - - // Generate available networks table (show up to 8 networks) - const availableNetworks = []; - const networksToShow = Math.min(8, Number(totalPossibleSubnets)); - - for (let i = 0; i < networksToShow; i++) { - const networkAddr = networkLong + (BigInt(i) * (BigInt(2) ** BigInt(128 - ipv6Cidr))); - const broadcastAddr = networkAddr + (BigInt(2) ** BigInt(128 - ipv6Cidr)) - BigInt(1); - - const networkStr = longToIPv6(networkAddr); - const broadcastStr = longToIPv6(broadcastAddr); - availableNetworks.push({ - network: networkStr, - networkCompressed: compressIPv6(networkStr), - broadcast: broadcastStr, - broadcastCompressed: compressIPv6(broadcastStr) - }); - } - - // Format large numbers for display - function formatBigInt(num) { - if (num < BigInt(1e6)) { - return num.toString(); - } else if (num < BigInt(1e9)) { - return (Number(num) / 1e6).toFixed(1) + 'M'; - } else if (num < BigInt(1e12)) { - return (Number(num) / 1e9).toFixed(1) + 'B'; - } else { - return Number(num).toExponential(2); - } - } - - out.innerHTML = ` -
- IPv6 Subnet Information -
- -
-
-

Input Information

-
- IPv6 Address: ${ipv6Address} -
-
- Expanded Address: ${expandedIPv6} -
-
- Compressed Address: ${compressIPv6(expandedIPv6)} -
-
- CIDR Prefix: /${ipv6Cidr} -
-
- -
-

Network Information

-
- Network Address: ${longToIPv6(networkLong)} -
-
- Network Address (Compressed): ${compressIPv6(longToIPv6(networkLong))} -
-
- Broadcast Address: ${longToIPv6(broadcastLong)} -
-
- Broadcast Address (Compressed): ${compressIPv6(longToIPv6(broadcastLong))} -
-
- Address Space: ${128 - ipv6Cidr} bits -
-
-
- -
-

Host Information

-
- Total Hosts: ${formatBigInt(totalHosts)} -
-
- Subnets in /64: ${formatBigInt(totalPossibleSubnets)} -
-
- Host Bits: ${128 - ipv6Cidr} -
-
- Network Bits: ${ipv6Cidr} -
-
- -
-

Available Networks

-
- - - - - - - - - - - ${availableNetworks.map(net => ` - - - - - - - `).join('')} - -
Network (Expanded)Network (Compressed)Broadcast (Expanded)Broadcast (Compressed)
${net.network}${net.networkCompressed}${net.broadcast}${net.broadcastCompressed}
-
-
- Showing ${availableNetworks.length} of ${formatBigInt(totalPossibleSubnets)} possible networks in /64 -
-
- `; - } - - function calculate() { - const ipVersionSelect = ui.querySelector('[name=ipVersion]'); - if (!ipVersionSelect) { - console.log('IP version select not found, skipping calculation'); - return; - } - - const ipVersion = ipVersionSelect.value; - console.log('Calculate called with IP version:', ipVersion, 'Select element:', ipVersionSelect); - - if (ipVersion === 'ipv4') { - console.log('Calculating IPv4'); - calculateIPv4(); - } else if (ipVersion === 'ipv6') { - console.log('Calculating IPv6'); - calculateIPv6(); - } else { - console.log('Unknown IP version:', ipVersion, 'Defaulting to IPv4'); - calculateIPv4(); - } - } - - // Event listeners - ui.querySelector('[name=ipVersion]').addEventListener('change', (e) => { - const ipv4Inputs = ui.querySelector('#ipv4-inputs'); - const ipv6Inputs = ui.querySelector('#ipv6-inputs'); - - console.log('IP version changed to:', e.target.value); - console.log('IPv4 inputs element:', ipv4Inputs); - console.log('IPv6 inputs element:', ipv6Inputs); - - if (e.target.value === 'ipv4') { - ipv4Inputs.style.display = 'block'; - ipv6Inputs.style.display = 'none'; - console.log('Switched to IPv4 mode'); - console.log('IPv4 display style:', ipv4Inputs.style.display); - console.log('IPv6 display style:', ipv6Inputs.style.display); - } else { - ipv4Inputs.style.display = 'none'; - ipv6Inputs.style.display = 'block'; - console.log('Switched to IPv6 mode'); - console.log('IPv4 display style:', ipv4Inputs.style.display); - console.log('IPv6 display style:', ipv6Inputs.style.display); - - // Debug IPv6 input elements - const ipv6AddressInput = ipv6Inputs.querySelector('[name=ipv6Address]'); - const ipv6CidrInput = ipv6Inputs.querySelector('[name=ipv6Cidr]'); - console.log('IPv6 Address input found:', ipv6AddressInput); - console.log('IPv6 CIDR input found:', ipv6CidrInput); - if (ipv6AddressInput) { - console.log('IPv6 Address input properties:', { - disabled: ipv6AddressInput.disabled, - readonly: ipv6AddressInput.readOnly, - style: ipv6AddressInput.style.cssText, - offsetWidth: ipv6AddressInput.offsetWidth, - offsetHeight: ipv6AddressInput.offsetHeight - }); - } - } - - // Force recalculation immediately after switching modes - setTimeout(() => { - console.log('Recalculating after mode switch'); - calculate(); - }, 100); - }); - - // Simple initialization - setTimeout(() => { - const ipVersionSelect = ui.querySelector('[name=ipVersion]'); - if (ipVersionSelect) { - // Ensure the default value is set - if (!ipVersionSelect.value) { - ipVersionSelect.value = 'ipv4'; - } - console.log('IP Version select initialized with value:', ipVersionSelect.value); - } - }, 200); - - // IPv4 event listeners - ui.querySelector('[name=ipAddress]').addEventListener('input', (e) => { - const ipInput = e.target.value; - - // Check if IP address contains CIDR notation (e.g., 10.0.0.1/8) - if (ipInput.includes('/')) { - const [ipPart, cidrPart] = ipInput.split('/'); - const cidrValue = parseInt(cidrPart); - - // Validate CIDR value and update CIDR field - if (cidrValue >= 0 && cidrValue <= 32) { - const cidrInput = ui.querySelector('[name=cidr]'); - cidrInput.value = cidrValue; - - // Update subnet mask based on new CIDR - const mask = cidrToMask(cidrValue); - const subnetMaskInput = ui.querySelector('[name=subnetMask]'); - subnetMaskInput.value = mask; - - // Trigger calculation - setTimeout(calculate, 10); - } - } - - // Always call calculate for normal input - calculate(); - }); - - // Subnet mask input - sync with CIDR and recalculate - ui.querySelector('[name=subnetMask]').addEventListener('input', (e) => { - const mask = e.target.value; - if (validateIPv4(mask)) { - const calculatedCidr = maskToCidr(mask); - ui.querySelector('[name=cidr]').value = calculatedCidr; - // Force immediate calculation update - setTimeout(calculate, 10); - } - }); - - // CIDR input - sync with subnet mask and recalculate - ui.querySelector('[name=cidr]').addEventListener('input', (e) => { - const cidr = +e.target.value; - console.log('CIDR input changed to:', cidr); - - // Validate CIDR range - if (cidr >= 0 && cidr <= 32) { - const mask = cidrToMask(cidr); - ui.querySelector('[name=subnetMask]').value = mask; - console.log('Updated subnet mask to:', mask); - - // Force immediate calculation update - setTimeout(() => { - console.log('Recalculating after CIDR change'); - calculate(); - }, 10); - } else { - console.log('Invalid CIDR value:', cidr); - } - }); - - // IPv6 event listeners - use the already created input elements - ipv6AddressInput.addEventListener('input', () => { - console.log('IPv6 address input changed:', ipv6AddressInput.value); - calculate(); - }); - - ipv6CidrInput.addEventListener('input', () => { - console.log('IPv6 CIDR input changed:', ipv6CidrInput.value); - calculate(); - }); - - // Initial calculation - wait for DOM to be ready - setTimeout(() => { - console.log('Running initial calculation'); - - // Ensure default IPv4 value is set (Select Lite might override it) - const ipVersionSelect = ui.querySelector('[name=ipVersion]'); - if (ipVersionSelect) { - console.log('Before setting default - IP version select value:', ipVersionSelect.value); - ipVersionSelect.value = 'ipv4'; - console.log('After setting default - IP version select value:', ipVersionSelect.value); - - // Force the change event to ensure proper display - const event = new Event('change', { bubbles: true }); - ipVersionSelect.dispatchEvent(event); - } - - console.log('About to call calculate()'); - calculate(); - console.log('calculate() called'); - }, 300); - - root.append(ui); - } -} diff --git a/public/calculators/zfs.js b/public/calculators/zfs.js deleted file mode 100644 index c96e420..0000000 --- a/public/calculators/zfs.js +++ /dev/null @@ -1,390 +0,0 @@ -import {revive, persist, labelInput, labelSelect} from '/js/util.js'; - -export default { - id:'zfs', name:'ZFS Calculator', about:'Calculate ZFS pool configurations, performance tuning, and capacity planning.', - render(root){ - const key='calc_zfs_v1'; - const s = revive(key,{ - poolType: 'raidz2', - diskCount: 6, - diskSize: 4000, - diskSizeUnit: 'GB', - blockSize: '128K', - compression: 'lz4', - dedup: false, - ashift: 12, - arcMax: 8192, - arcMaxUnit: 'MB' - }); - - const ui = document.createElement('div'); - - // Pool configuration section - const poolSection = document.createElement('div'); - poolSection.innerHTML = ` -

Pool Configuration

-
-
- - -
-
- - -
-
- -
- - -
-
-
- - -
-
- `; - - // Performance tuning section - const perfSection = document.createElement('div'); - perfSection.innerHTML = ` -

Performance Tuning

-
-
- - -
-
- - -
-
- - -
-
- -
- - -
-
-
- `; - - ui.append(poolSection, perfSection); - - // Results section - const out = document.createElement('div'); - out.className = 'result'; - out.style.cssText = ` - margin: 20px 0; - padding: 15px; - background: var(--k-bg); - border-radius: 8px; - border-left: 4px solid var(--accent); - `; - ui.append(out); - - - // Calculation function - function calc(){ - const poolType = ui.querySelector('[name=poolType]').value; - const diskCount = +ui.querySelector('[name=diskCount]').value; - const diskSize = +ui.querySelector('[name=diskSize]').value; - const diskSizeUnit = ui.querySelector('[name=diskSizeUnit]').value; - const blockSize = ui.querySelector('[name=blockSize]').value; - const compression = ui.querySelector('[name=compression]').value; - const dedup = ui.querySelector('[name=dedup]').value === 'true'; - const ashift = +ui.querySelector('[name=ashift]').value; - const arcMax = +ui.querySelector('[name=arcMax]').value; - const arcMaxUnit = ui.querySelector('[name=arcMaxUnit]').value; - - // Convert disk size to GB for calculations - let diskSizeGB = diskSize; - if (diskSizeUnit === 'TB') { - diskSizeGB = diskSize * 1024; - } - - // Calculate usable capacity based on pool type - let usableCapacity, redundancy, minDisks, recommendedDisks, vdevCount; - - switch(poolType) { - case 'stripe': - usableCapacity = diskCount * diskSizeGB; - redundancy = 'None'; - minDisks = 1; - recommendedDisks = 1; - vdevCount = 1; - break; - case 'mirror': - usableCapacity = Math.floor(diskCount / 2) * diskSizeGB; - redundancy = '50%'; - minDisks = 2; - recommendedDisks = 2; - vdevCount = Math.floor(diskCount / 2); - break; - case 'raidz1': - usableCapacity = (diskCount - 1) * diskSizeGB; - redundancy = '1 disk'; - minDisks = 3; - recommendedDisks = 3; - vdevCount = 1; - break; - case 'raidz2': - usableCapacity = (diskCount - 2) * diskSizeGB; - redundancy = '2 disks'; - minDisks = 4; - recommendedDisks = 6; - vdevCount = 1; - break; - case 'raidz3': - usableCapacity = (diskCount - 3) * diskSizeGB; - redundancy = '3 disks'; - minDisks = 5; - recommendedDisks = 9; - vdevCount = 1; - break; - case 'mirror2x2': - usableCapacity = 2 * diskSizeGB; // 2 mirrors, each with 1 usable disk - redundancy = '50%'; - minDisks = 4; - recommendedDisks = 4; - vdevCount = 2; - break; - case 'mirror3x2': - usableCapacity = 3 * diskSizeGB; // 3 mirrors, each with 1 usable disk - redundancy = '50%'; - minDisks = 6; - recommendedDisks = 6; - vdevCount = 3; - break; - case 'raidz2x2': - usableCapacity = 2 * (diskCount / 2 - 2) * diskSizeGB; // 2 RAID-Z2 vdevs - redundancy = '2 disks per vdev'; - minDisks = 8; - recommendedDisks = 12; - vdevCount = 2; - break; - } - - // Validate disk count - if (diskCount < minDisks) { - out.innerHTML = `
- Error: ${poolType.toUpperCase()} requires at least ${minDisks} disks -
`; - return; - } - - // Calculate compression ratios - const compressionRatios = { - 'off': 1.0, - 'lz4': 2.1, - 'gzip': 2.5, - 'gzip-1': 2.0, - 'gzip-9': 3.0, - 'zstd': 2.8, - 'zstd-1': 2.2, - 'zstd-19': 3.5 - }; - - const compressionRatio = compressionRatios[compression] || 1.0; - const effectiveCapacity = usableCapacity * compressionRatio; - - // Calculate performance metrics - const blockSizeKB = parseInt(blockSize.replace(/[^0-9]/g, '')); - const ashiftBytes = Math.pow(2, ashift); - - // Convert ARC max to MB - let arcMaxMB = arcMax; - if (arcMaxUnit === 'GB') { - arcMaxMB = arcMax * 1024; - } - - // Helper function to format capacity - function formatCapacity(gb) { - if (gb >= 1024) { - return `${gb.toFixed(1)} GB (${(gb / 1024).toFixed(2)} TB)`; - } - return `${gb.toFixed(1)} GB`; - } - - // Calculate I/O performance estimates - const estimatedIOPS = Math.floor(diskCount * 100); // Rough estimate: 100 IOPS per disk - const estimatedThroughput = Math.floor(diskCount * 150); // Rough estimate: 150 MB/s per disk - - // Calculate memory requirements (rule of thumb: 1GB RAM per 1TB storage) - const recommendedRAM = Math.ceil((diskCount * diskSizeGB) / 1024); - - // Generate results - out.innerHTML = ` -
- ZFS Pool Configuration -
- -
-
-

Capacity & Redundancy

-
- Raw Capacity: ${formatCapacity(diskCount * diskSizeGB)} -
-
- Usable Capacity: ${formatCapacity(usableCapacity)} -
-
- Effective Capacity (with compression): ${formatCapacity(effectiveCapacity)} -
-
- Redundancy: ${redundancy} -
-
- Efficiency: ${((usableCapacity / (diskCount * diskSizeGB)) * 100).toFixed(1)}% -
-
- Compression Savings: ${((effectiveCapacity - usableCapacity) / usableCapacity * 100).toFixed(1)}% -
-
- VDev Count: ${vdevCount} ${vdevCount > 1 ? 'vdevs' : 'vdev'} -
-
- -
-

Performance Settings

-
- Block Size: ${blockSize} -
-
- Ashift: ${ashift} (${ashiftBytes} bytes) -
-
- Compression: ${compression} (${compressionRatio.toFixed(1)}x ratio) -
-
- Deduplication: ${dedup ? 'On' : 'Off'} -
-
- ARC Max: ${arcMaxMB} MB -
-
-
- -
-
-

Performance Estimates

-
- Estimated IOPS: ${estimatedIOPS.toLocaleString()} (random 4K reads) -
-
- Estimated Throughput: ${estimatedThroughput} MB/s (sequential) -
-
- Recommended RAM: ${recommendedRAM} GB minimum -
-
- Current ARC: ${(arcMaxMB / 1024).toFixed(1)} GB -
-
- -
-

System Requirements

-
- Minimum RAM: ${Math.max(8, recommendedRAM)} GB -
-
- Recommended RAM: ${Math.max(16, recommendedRAM * 2)} GB -
-
- CPU Cores: ${Math.max(4, Math.ceil(diskCount / 2))} cores recommended -
-
- Network: 10 Gbps recommended for ${estimatedThroughput} MB/s throughput -
-
-
- - - - `; - - persist(key, {poolType, diskCount, diskSize, diskSizeUnit, blockSize, compression, dedup, ashift, arcMax, arcMaxUnit}); - } - - // Event listeners - ui.querySelector('[name=poolType]').addEventListener('change', calc); - ui.querySelector('[name=diskCount]').addEventListener('input', calc); - ui.querySelector('[name=diskSize]').addEventListener('input', calc); - ui.querySelector('[name=diskSizeUnit]').addEventListener('change', calc); - ui.querySelector('[name=blockSize]').addEventListener('change', calc); - ui.querySelector('[name=compression]').addEventListener('change', calc); - ui.querySelector('[name=dedup]').addEventListener('change', calc); - ui.querySelector('[name=ashift]').addEventListener('change', calc); - ui.querySelector('[name=arcMax]').addEventListener('input', calc); - ui.querySelector('[name=arcMaxUnit]').addEventListener('change', calc); - - // Initial calculation - calc(); - root.append(ui); - } -} diff --git a/public/css/styles.css b/public/css/styles.css index 77f57af..a11f1c9 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -65,135 +65,16 @@ select:disabled{ opacity:.55; cursor:not-allowed; } html,body{margin:0;background:var(--bg);color:var(--text);font:16px/1.5 system-ui,Segoe UI,Roboto,Ubuntu,Cantarell,sans-serif} .wrap{max-width:var(--max);margin:0 auto;padding:16px} -.bar{position:sticky;top:0;background:linear-gradient(180deg,rgba(0,0,0,.06),rgba(0,0,0,0));backdrop-filter:blur(8px);border-bottom:1px solid var(--border);z-index:10;min-height:70px} -.bar__inner{display:flex;align-items:center;gap:12px;justify-content:space-between;min-height:70px} +.bar{position:sticky;top:0;background:linear-gradient(180deg,rgba(0,0,0,.06),rgba(0,0,0,0));backdrop-filter:blur(8px);border-bottom:1px solid var(--border);z-index:10} +.bar__inner{display:flex;align-items:center;gap:12px;justify-content:space-between} .brand{font-weight:700} .btn{background:transparent;border:1px solid var(--border);color:var(--text);padding:8px 10px;border-radius:999px;cursor:pointer} -/* Mobile navigation toggle button */ -.nav-toggle { - display: none; - background: transparent; - border: 1px solid var(--border); - color: var(--text); - padding: 8px; - border-radius: 8px; - cursor: pointer; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; -} - -.nav-toggle svg { - width: 20px; - height: 20px; - fill: currentColor; -} - /* ---- Layout ---- */ .layout{display:grid;grid-template-columns:240px 1fr;gap:16px} @media (max-width: 820px){ .layout{grid-template-columns:1fr} - - /* Show mobile nav toggle */ - .nav-toggle { - display: flex !important; - } - - /* Hide desktop navigation by default on mobile */ - .sidenav { - display: none; - position: fixed; - top: 70px; - left: 0; - right: 0; - bottom: 0; - z-index: 100; - background: var(--card); - border-radius: 0; - border: none; - box-shadow: var(--shadow); - overflow-y: auto; - } - - /* Show navigation when active */ - .sidenav.mobile-active { - display: block; - } - - /* Add mobile overlay */ - .sidenav::before { - content: ''; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - z-index: -1; - } - - /* Adjust main content spacing for mobile */ - .content { - margin-top: 16px; - } - - /* Improve mobile spacing */ - .wrap { - padding: 12px; - } - - /* Better mobile grid */ - .content { - grid-template-columns: 1fr; - gap: 12px; - } - - /* Mobile-friendly cards */ - .card { - padding: 12px; - } - - /* Mobile-friendly inputs */ - input, select, textarea { - padding: 12px; - font-size: 16px; /* Prevents zoom on iOS */ - } - - /* Mobile-friendly results */ - .result { - margin-top: 16px; - padding: 12px; - overflow-x: auto; - } - - /* Mobile-friendly tables */ - table { - font-size: 14px; - } - - /* Mobile-friendly calculator inputs */ - .calculator-container { - padding: 16px 0; - } - - /* Ensure proper spacing from navigation */ - .layout { - padding-top: 16px; - } - - /* Mobile-friendly footer */ - .footer-content { - flex-direction: column; - gap: 12px; - text-align: center; - } - - .source-link { - margin-left: 0; - } } /* ---- Vertical nav ---- */ @@ -270,25 +151,6 @@ input,select,textarea{width:100%;background:transparent;color:var(--text);border .k{padding:2px 6px;border-radius:6px;border:1px solid var(--k-border);background:var(--k-bg)} .foot{color:var(--muted);font-size:13px;margin-top:20px} -.footer-content { - display: flex; - justify-content: space-between; - align-items: center; -} - -.source-link { - color: var(--accent); - text-decoration: none; - font-weight: 500; - transition: color 0.2s ease; - margin-left: auto; -} - -.source-link:hover { - color: var(--accent2); - text-decoration: underline; -} - /* ---- Status indicators ---- */ .status { padding: 8px 12px; diff --git a/public/index.html b/public/index.html index b21196e..e16d5a1 100644 --- a/public/index.html +++ b/public/index.html @@ -14,14 +14,7 @@
calculator.127local.net
-
- - -
+
@@ -30,12 +23,7 @@
- + diff --git a/public/js/app.js b/public/js/app.js index b009f43..ab5182a 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -5,37 +5,14 @@ const CALCS = [ { id:'raid', name:'RAID', about:'Usable capacity', path:'../calculators/raid.js' }, { id:'bandwidth', name:'Bandwidth', about:'Bits↔bytes unit conv.', path:'../calculators/bandwidth.js' }, { id:'nmea', name:'NMEA', about:'0183 XOR checksum', path:'../calculators/nmea.js' }, - { id:'currency', name:'Currency Converter', about:'Convert between currencies', path:'../calculators/currency.js' }, - { id:'zfs', name:'ZFS', about:'Pool configuration & performance', path:'../calculators/zfs.js' }, - { id:'subnet', name:'IP Subnet', about:'IPv4/IPv6 subnet calculations', path:'../calculators/subnet.js' } + { id:'currency', name:'Currency Converter', about:'Convert between currencies', path:'../calculators/currency.js' } ]; const navEl = document.getElementById('nav'); const viewEl = document.getElementById('view'); const themeBtn= document.getElementById('themeToggle'); -const navToggleBtn = document.getElementById('navToggle'); initTheme(themeBtn); -// Mobile navigation toggle -navToggleBtn.addEventListener('click', () => { - navEl.classList.toggle('mobile-active'); -}); - -// Close mobile nav when clicking outside -document.addEventListener('click', (e) => { - if (!navEl.contains(e.target) && !navToggleBtn.contains(e.target)) { - navEl.classList.remove('mobile-active'); - } -}); - -// Close mobile nav when clicking on a nav link -navEl.addEventListener('click', (e) => { - const a = e.target.closest('a[data-calc]'); - if (a) { - navEl.classList.remove('mobile-active'); - } -}); - const moduleCache = new Map(); const viewCache = new Map(); @@ -131,10 +108,6 @@ async function ensureMounted(id){ card.innerHTML = ''; card.append(el('h2',{}, calc.name || meta.name)); if(calc.about) card.append(el('div',{class:'muted'}, calc.about)); - - // Update page title with calculator name - document.title = `${calc.name || meta.name} - calculator.127local.net`; - calc.render(card); enhanceSelects(card); }catch(e){ @@ -168,10 +141,6 @@ async function show(id, params){ const card = await ensureMounted(id); viewEl.append(card); attachUrlSync(card, id); - } else { - // Update title for already cached calculators - const meta = metaById(id); - document.title = `${meta.name} - calculator.127local.net`; } paramsToForm(viewCache.get(id), params); } diff --git a/tests/conftest.py b/tests/conftest.py index 2527e61..812db34 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,8 +3,6 @@ import pytest import pathlib import sys import requests -from selenium import webdriver -from selenium.webdriver.chrome.options import Options from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.by import By @@ -98,22 +96,6 @@ def dev_server(): print("Development server stopped") -@pytest.fixture(scope="function") -def driver(): - """Set up Chrome driver with options""" - options = Options() - options.add_argument("--headless") - options.add_argument("--no-sandbox") - options.add_argument("--disable-dev-shm-usage") - options.add_argument("--disable-gpu") - options.add_argument("--window-size=1920,1080") - - driver = webdriver.Chrome(options=options) - driver.implicitly_wait(10) - yield driver - driver.quit() - - @pytest.fixture(scope="function") def calculator_page(driver, dev_server): """Navigate to the calculator page using the development server""" diff --git a/tests/test_mobile.py b/tests/test_mobile.py deleted file mode 100644 index b0ee845..0000000 --- a/tests/test_mobile.py +++ /dev/null @@ -1,344 +0,0 @@ -import pytest -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.common.action_chains import ActionChains - - -class TestMobileResponsiveness: - """Test mobile responsiveness and navigation functionality""" - - def test_mobile_nav_toggle_button_exists(self, calculator_page): - """Test that mobile navigation toggle button is present""" - # Set mobile viewport - calculator_page.set_window_size(375, 667) - - nav_toggle = calculator_page.find_element(By.ID, "navToggle") - assert nav_toggle.is_displayed() - - # Check it has the correct class - assert "nav-toggle" in nav_toggle.get_attribute("class") - - # Check it has the hamburger icon - svg = nav_toggle.find_element(By.TAG_NAME, "svg") - assert svg.is_displayed() - - def test_mobile_nav_toggle_functionality(self, calculator_page): - """Test that mobile navigation toggle works correctly""" - # Set mobile viewport - calculator_page.set_window_size(375, 667) - - nav_toggle = calculator_page.find_element(By.ID, "navToggle") - sidenav = calculator_page.find_element(By.ID, "nav") - - # Debug: check if element is actually clickable - print(f"Nav toggle displayed: {nav_toggle.is_displayed()}") - print(f"Nav toggle enabled: {nav_toggle.is_enabled()}") - print(f"Nav toggle location: {nav_toggle.location}") - print(f"Nav toggle size: {nav_toggle.size}") - - # Initially, sidenav should not have mobile-active class - assert "mobile-active" not in sidenav.get_attribute("class") - - # Wait for element to be clickable - WebDriverWait(calculator_page, 10).until( - EC.element_to_be_clickable((By.ID, "navToggle")) - ) - - # Click the toggle button using JavaScript if regular click fails - try: - nav_toggle.click() - except Exception as e: - print(f"Regular click failed: {e}") - calculator_page.execute_script("arguments[0].click();", nav_toggle) - - # Wait for the mobile-active class to be added - WebDriverWait(calculator_page, 5).until( - EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active")) - ) - - # Click again to close - try: - nav_toggle.click() - except Exception as e: - print(f"Regular click failed on close: {e}") - calculator_page.execute_script("arguments[0].click();", nav_toggle) - - # Wait for the mobile-active class to be removed - WebDriverWait(calculator_page, 5).until_not( - EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active")) - ) - - def test_mobile_nav_closes_on_outside_click(self, calculator_page): - """Test that mobile navigation closes when clicking outside""" - # Set mobile viewport - calculator_page.set_window_size(375, 667) - - nav_toggle = calculator_page.find_element(By.ID, "navToggle") - sidenav = calculator_page.find_element(By.ID, "nav") - - # Open mobile nav using JavaScript - calculator_page.execute_script("arguments[0].click();", nav_toggle) - - # Wait for nav to open - WebDriverWait(calculator_page, 5).until( - EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active")) - ) - - # Click on the body element (outside nav) using JavaScript - calculator_page.execute_script("document.body.click();") - - # Wait for nav to close - WebDriverWait(calculator_page, 5).until_not( - EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active")) - ) - - def test_mobile_nav_closes_on_nav_link_click(self, calculator_page): - """Test that mobile navigation closes when clicking a navigation link""" - # Set mobile viewport - calculator_page.set_window_size(375, 667) - - nav_toggle = calculator_page.find_element(By.ID, "navToggle") - sidenav = calculator_page.find_element(By.ID, "nav") - - # Open mobile nav using JavaScript - calculator_page.execute_script("arguments[0].click();", nav_toggle) - - # Wait for nav to open - WebDriverWait(calculator_page, 5).until( - EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active")) - ) - - # Click on a navigation link - nav_link = sidenav.find_element(By.CSS_SELECTOR, "a[data-calc='raid']") - nav_link.click() - - # Wait for nav to close - WebDriverWait(calculator_page, 5).until_not( - EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active")) - ) - - def test_mobile_nav_sticky_positioning(self, calculator_page): - """Test that navigation bar stays at top and content doesn't scroll under it""" - # Get the navigation bar - nav_bar = calculator_page.find_element(By.CLASS_NAME, "bar") - - # Check that it has sticky positioning - position = nav_bar.value_of_css_property("position") - assert position == "sticky" - - # Check that it has a high z-index - z_index = nav_bar.value_of_css_property("z-index") - assert int(z_index) >= 10 - - # Check that it has a minimum height - min_height = nav_bar.value_of_css_property("min-height") - assert min_height == "70px" - - def test_mobile_responsive_layout(self, calculator_page): - """Test that layout changes appropriately on mobile""" - # Set mobile viewport - calculator_page.set_window_size(375, 667) - - # Get the layout container - layout = calculator_page.find_element(By.CLASS_NAME, "layout") - - # Check that it has proper grid layout - display = layout.value_of_css_property("display") - assert display == "grid" - - # Check that it has responsive grid template - grid_template = layout.value_of_css_property("grid-template-columns") - # Should be responsive - on mobile it will be 1fr, on desktop 240px 1fr - # The actual value might be computed differently, so just check it's a valid grid value - assert "px" in grid_template or "fr" in grid_template - - def test_mobile_friendly_inputs(self, calculator_page): - """Test that inputs are mobile-friendly""" - # Set mobile viewport - calculator_page.set_window_size(375, 667) - - # Navigate to a calculator with inputs - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # Check input styling - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - - # Check padding (should be 12px on mobile) - padding = ip_input.value_of_css_property("padding") - assert "12px" in padding - - # Check font size (should be 16px to prevent zoom on iOS) - font_size = ip_input.value_of_css_property("font-size") - assert "16px" in font_size - - def test_mobile_table_overflow(self, calculator_page): - """Test that tables have horizontal scroll on mobile""" - # Set mobile viewport - calculator_page.set_window_size(375, 667) - - # Navigate to subnet calculator which has tables - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # Enter an IP address to generate the table - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - ip_input.clear() - ip_input.send_keys("192.168.1.1") - - # Wait for table to appear - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.TAG_NAME, "table")) - ) - - # Check that the result container has overflow handling - result_container = calculator_page.find_element(By.CLASS_NAME, "result") - overflow_x = result_container.value_of_css_property("overflow-x") - # Should have auto or scroll overflow on mobile - assert overflow_x in ["auto", "scroll"] - - def test_mobile_footer_layout(self, calculator_page): - """Test that footer is mobile-friendly""" - footer_content = calculator_page.find_element(By.CLASS_NAME, "footer-content") - - # Check that footer content has proper flexbox layout - display = footer_content.value_of_css_property("display") - assert display == "flex" - - # Check that source link is properly positioned - source_link = calculator_page.find_element(By.CLASS_NAME, "source-link") - assert source_link.is_displayed() - assert "https://code.disobey.net/whilb/calculator.127local.net" in source_link.get_attribute("href") - - def test_mobile_nav_theme_toggle_buttons(self, calculator_page): - """Test that both nav toggle and theme toggle buttons are accessible""" - # Set mobile viewport - calculator_page.set_window_size(375, 667) - - nav_toggle = calculator_page.find_element(By.ID, "navToggle") - theme_toggle = calculator_page.find_element(By.ID, "themeToggle") - - # Both buttons should be visible - assert nav_toggle.is_displayed() - assert theme_toggle.is_displayed() - - # Both should be clickable - assert nav_toggle.is_enabled() - assert theme_toggle.is_enabled() - - # Check button styling - for button in [nav_toggle, theme_toggle]: - cursor = button.value_of_css_property("cursor") - assert cursor == "pointer" - - def test_mobile_nav_accessibility(self, calculator_page): - """Test mobile navigation accessibility features""" - nav_toggle = calculator_page.find_element(By.ID, "navToggle") - sidenav = calculator_page.find_element(By.ID, "nav") - - # Check aria-label on toggle button - aria_label = nav_toggle.get_attribute("aria-label") - assert aria_label == "Toggle navigation" - - # Check that sidenav has proper role (should be navigation) - role = sidenav.get_attribute("role") - # If no explicit role, check that it's semantically correct - if not role: - # Should contain navigation links - nav_links = sidenav.find_elements(By.CSS_SELECTOR, "a[data-calc]") - assert len(nav_links) > 0 - - def test_mobile_nav_calculator_integration(self, calculator_page): - """Test that mobile navigation works properly with calculator functionality""" - # Set mobile viewport - calculator_page.set_window_size(375, 667) - - # Navigate to subnet calculator - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # Open mobile navigation using JavaScript - nav_toggle = calculator_page.find_element(By.ID, "navToggle") - calculator_page.execute_script("arguments[0].click();", nav_toggle) - - # Wait for nav to open - WebDriverWait(calculator_page, 5).until( - EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active")) - ) - - # Navigate to a different calculator via mobile nav - nav_link = calculator_page.find_element(By.CSS_SELECTOR, "a[data-calc='currency']") - nav_link.click() - - # Wait for nav to close and currency calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='amount']")) - ) - - # Verify we're on the currency calculator - currency_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='amount']") - assert currency_input.is_displayed() - - # Verify nav is closed - sidenav = calculator_page.find_element(By.ID, "nav") - assert "mobile-active" not in sidenav.get_attribute("class") - - def test_mobile_nav_scroll_behavior(self, calculator_page): - """Test that mobile navigation doesn't interfere with page scrolling""" - # Set mobile viewport - calculator_page.set_window_size(375, 667) - - # Navigate to a calculator with long content - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # Enter an IP address to generate content - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - ip_input.clear() - ip_input.send_keys("10.0.0.1") - - # Wait for results to appear - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "result")) - ) - - # Open mobile navigation using JavaScript - nav_toggle = calculator_page.find_element(By.ID, "navToggle") - calculator_page.execute_script("arguments[0].click();", nav_toggle) - - # Wait for nav to open - WebDriverWait(calculator_page, 5).until( - EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active")) - ) - - # Try to scroll the page - calculator_page.execute_script("window.scrollTo(0, 100)") - - # Verify navigation is still open and functional - sidenav = calculator_page.find_element(By.ID, "nav") - assert "mobile-active" in sidenav.get_attribute("class") - - # Close navigation using JavaScript - calculator_page.execute_script("arguments[0].click();", nav_toggle) - - # Verify navigation closes - WebDriverWait(calculator_page, 5).until_not( - EC.presence_of_element_located((By.CSS_SELECTOR, ".sidenav.mobile-active")) - ) diff --git a/tests/test_subnet.py b/tests/test_subnet.py deleted file mode 100644 index 759420b..0000000 --- a/tests/test_subnet.py +++ /dev/null @@ -1,817 +0,0 @@ -import pytest -import time -import requests -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.common.by import By -from selenium.webdriver.common.keys import Keys - - -class TestSubnetCalculator: - """Comprehensive tests for the IP Subnet Calculator""" - - def test_subnet_ipv4_basic_calculation(self, calculator_page): - """Test basic IPv4 subnet calculation with known values""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # Test with a known /24 network - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - ip_input.clear() - ip_input.send_keys("192.168.1.0") - - cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='cidr']") - cidr_input.clear() - cidr_input.send_keys("24") - - # Wait for results - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "result")) - ) - - result_text = self._get_subnet_result(calculator_page) - - # Verify network address - assert "Network Address: 192.168.1.0" in result_text - # Verify broadcast address - assert "Broadcast Address: 192.168.1.255" in result_text - # Verify total hosts (2^8 - 2 = 254) - assert "Total Hosts: 254" in result_text - # Verify first usable host - assert "First Usable Host: 192.168.1.1" in result_text - # Verify last usable host - assert "Last Usable Host: 192.168.1.254" in result_text - - def test_subnet_ipv4_cidr_edge_cases(self, calculator_page): - """Test IPv4 CIDR edge cases and boundary conditions""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # Test /32 (single host) - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='cidr']") - - ip_input.clear() - ip_input.send_keys("10.0.0.1") - cidr_input.clear() - cidr_input.send_keys("32") - - # Wait for results - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "result")) - ) - - result_text = self._get_subnet_result(calculator_page) - - # Debug: print what we actually got - print(f"Result text for /32: {result_text}") - - # Check what CIDR was actually applied - cidr_value = cidr_input.get_attribute("value") - print(f"CIDR input value: {cidr_value}") - - # The calculator seems to have a bug where /32 becomes /0 - # Let's test the actual behavior and document it - if "Total Hosts: 4,294,967,294" in result_text: - # This is /0 behavior (2^32 - 2) - print("Calculator is treating /32 as /0 - this is a bug") - # For now, let's test what actually happens - assert "Total Hosts: 4,294,967,294" in result_text - else: - # If it's working correctly, /32 should have 1 total host - assert "Total Hosts: 1" in result_text - - # Test /31 (point-to-point, no usable hosts) - cidr_input.clear() - cidr_input.send_keys("31") - - # Wait for results to update - WebDriverWait(calculator_page, 10).until( - lambda driver: "Total Hosts:" in self._get_subnet_result(driver) - ) - - result_text = self._get_subnet_result(calculator_page) - print(f"Result text for /31: {result_text}") - - # Check what CIDR was actually applied - cidr_value = cidr_input.get_attribute("value") - print(f"CIDR input value for /31: {cidr_value}") - - # Test /30 (smallest usable subnet) - cidr_input.clear() - cidr_input.send_keys("30") - - # Wait for results to update - WebDriverWait(calculator_page, 10).until( - lambda driver: "Total Hosts:" in self._get_subnet_result(driver) - ) - - result_text = self._get_subnet_result(calculator_page) - print(f"Result text for /30: {result_text}") - - # Check what CIDR was actually applied - cidr_value = cidr_input.get_attribute("value") - print(f"CIDR input value for /30: {cidr_value}") - - # For /30, we should get 4 total hosts and 2 usable - if "Total Hosts: 4" in result_text: - assert "Total Hosts: 4" in result_text - assert "First Usable Host: 10.0.0.1" in result_text - assert "Last Usable Host: 10.0.0.2" in result_text - else: - print(f"Unexpected result for /30: {result_text}") - # Let's just verify we get some result - assert "Total Hosts:" in result_text - - def test_subnet_ipv4_network_class_detection(self, calculator_page): - """Test IPv4 network class detection""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='cidr']") - - # Test Class A - ip_input.clear() - ip_input.send_keys("10.0.0.1") - cidr_input.clear() - cidr_input.send_keys("8") - - # Wait for results - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "result")) - ) - - result_text = self._get_subnet_result(calculator_page) - assert "Network Class: Class A" in result_text - - # Test Class B - ip_input.clear() - ip_input.send_keys("172.16.0.1") - cidr_input.clear() - cidr_input.send_keys("16") - - # Wait for results to update - WebDriverWait(calculator_page, 10).until( - lambda driver: "Network Class: Class B" in self._get_subnet_result(driver) - ) - - result_text = self._get_subnet_result(calculator_page) - assert "Network Class: Class B" in result_text - - # Test Class C - ip_input.clear() - ip_input.send_keys("192.168.1.1") - cidr_input.clear() - cidr_input.send_keys("24") - - # Wait for results to update - WebDriverWait(calculator_page, 10).until( - lambda driver: "Network Class: Class C" in self._get_subnet_result(driver) - ) - - result_text = self._get_subnet_result(calculator_page) - assert "Network Class: Class C" in result_text - - def test_subnet_ipv4_binary_representation(self, calculator_page): - """Test IPv4 binary representation accuracy""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='cidr']") - - # Test with a simple IP for easy binary verification - ip_input.clear() - ip_input.send_keys("192.168.1.1") - cidr_input.clear() - cidr_input.send_keys("24") - - # Wait for results - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "result")) - ) - - result_text = self._get_subnet_result(calculator_page) - - # Verify binary representation - # 192 = 11000000, 168 = 10101000, 1 = 00000001 - assert "IP Address: 11000000.10101000.00000001.00000001" in result_text - # Subnet mask 255.255.255.0 = 11111111.11111111.11111111.00000000 - assert "Subnet Mask: 11111111.11111111.11111111.00000000" in result_text - - # Verify hexadecimal representation - assert "(0xC0A80101)" in result_text # 192.168.1.1 in hex - assert "(0xFFFFFF00)" in result_text # 255.255.255.0 in hex - - def test_subnet_ipv4_available_networks_table(self, calculator_page): - """Test IPv4 available networks table accuracy""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='cidr']") - - # Test with /24 to get a reasonable number of networks - ip_input.clear() - ip_input.send_keys("192.168.0.1") - cidr_input.clear() - cidr_input.send_keys("24") - - # Wait for results - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "result")) - ) - - result_text = self._get_subnet_result(calculator_page) - - # Should show 64 networks (as per our implementation) - assert "Showing 64 of" in result_text - - # Verify first few networks are correct - assert "192.168.0.0" in result_text - assert "192.168.1.0" in result_text - assert "192.168.2.0" in result_text - - # Verify network information is complete - assert "First Host" in result_text - assert "Last Host" in result_text - assert "Broadcast" in result_text - - def test_subnet_ipv4_cidr_subnet_mask_sync(self, calculator_page): - """Test bidirectional sync between CIDR and Subnet Mask inputs""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='cidr']") - subnet_mask_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='subnetMask']") - - # Test CIDR to Subnet Mask sync - cidr_input.clear() - cidr_input.send_keys("16") - - # Wait for subnet mask to update - WebDriverWait(calculator_page, 10).until( - lambda driver: subnet_mask_input.get_attribute("value") == "255.255.0.0" - ) - - assert subnet_mask_input.get_attribute("value") == "255.255.0.0" - - # Test Subnet Mask to CIDR sync - subnet_mask_input.clear() - subnet_mask_input.send_keys("255.255.255.128") - - # Wait for CIDR to update - WebDriverWait(calculator_page, 10).until( - lambda driver: cidr_input.get_attribute("value") == "25" - ) - - assert cidr_input.get_attribute("value") == "25" - - # Test edge case: /31 - cidr_input.clear() - cidr_input.send_keys("31") - - # Wait for subnet mask to update - WebDriverWait(calculator_page, 10).until( - lambda driver: subnet_mask_input.get_attribute("value") == "255.255.255.254" - ) - - assert subnet_mask_input.get_attribute("value") == "255.255.255.254" - - def test_subnet_ipv4_cidr_in_ip_input(self, calculator_page): - """Test parsing CIDR notation from IP address input""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='cidr']") - - # Test IP with CIDR notation - ip_input.clear() - ip_input.send_keys("10.0.0.1/8") - - # Wait for CIDR to be populated - WebDriverWait(calculator_page, 10).until( - lambda driver: cidr_input.get_attribute("value") == "8" - ) - - assert cidr_input.get_attribute("value") == "8" - - # Test another CIDR value - ip_input.clear() - ip_input.send_keys("172.16.0.1/16") - - # Wait for CIDR to update - WebDriverWait(calculator_page, 10).until( - lambda driver: cidr_input.get_attribute("value") == "16" - ) - - assert cidr_input.get_attribute("value") == "16" - - # Test edge case: /32 - ip_input.clear() - ip_input.send_keys("192.168.1.1/32") - - # Wait for CIDR to update - WebDriverWait(calculator_page, 10).until( - lambda driver: cidr_input.get_attribute("value") == "32" - ) - - assert cidr_input.get_attribute("value") == "32" - - def test_subnet_ipv6_basic_calculation(self, calculator_page): - """Test basic IPv6 subnet calculation""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # Switch to IPv6 - ip_version_select = calculator_page.find_element(By.CSS_SELECTOR, "select[name='ipVersion']") - calculator_page.execute_script("arguments[0].value = 'ipv6'; arguments[0].dispatchEvent(new Event('change'));", ip_version_select) - - # Wait for IPv6 inputs to appear - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipv6Address']")) - ) - - # Add a small delay to ensure inputs are fully ready - import time - time.sleep(0.5) - - # Verify we're actually in IPv6 mode by checking the display - ipv4_inputs = calculator_page.find_element(By.CSS_SELECTOR, "#ipv4-inputs") - ipv6_inputs = calculator_page.find_element(By.CSS_SELECTOR, "#ipv6-inputs") - - print(f"IPv4 inputs display style: {ipv4_inputs.get_attribute('style')}") - print(f"IPv6 inputs display style: {ipv6_inputs.get_attribute('style')}") - - # Force IPv6 mode if needed - if 'display: none' not in ipv6_inputs.get_attribute('style'): - print("Forcing IPv6 mode...") - calculator_page.execute_script(""" - document.getElementById('ipv4-inputs').style.display = 'none'; - document.getElementById('ipv6-inputs').style.display = 'block'; - """) - - # Also force the select value and trigger calculation - calculator_page.execute_script(""" - const select = document.querySelector('select[name="ipVersion"]'); - select.value = 'ipv6'; - select.dispatchEvent(new Event('change', { bubbles: true })); - """) - - # Wait a moment for the mode switch to take effect - time.sleep(0.5) - - # Check display states again - ipv4_inputs = calculator_page.find_element(By.CSS_SELECTOR, "#ipv4-inputs") - ipv6_inputs = calculator_page.find_element(By.CSS_SELECTOR, "#ipv6-inputs") - print(f"After forcing - IPv4 inputs display style: {ipv4_inputs.get_attribute('style')}") - print(f"After forcing - IPv6 inputs display style: {ipv6_inputs.get_attribute('style')}") - - ipv6_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipv6Address']") - ipv6_cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipv6Cidr']") - - # Test with a known IPv6 network - # Use JavaScript to interact with inputs since they seem to have interaction issues - calculator_page.execute_script("arguments[0].value = '2001:db8::';", ipv6_input) - calculator_page.execute_script("arguments[0].value = '64';", ipv6_cidr_input) - - # Trigger the change events manually - calculator_page.execute_script("arguments[0].dispatchEvent(new Event('input', { bubbles: true }));", ipv6_input) - calculator_page.execute_script("arguments[0].dispatchEvent(new Event('input', { bubbles: true }));", ipv6_cidr_input) - - # Wait for results - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "result")) - ) - - result_text = self._get_subnet_result(calculator_page) - - # Verify IPv6 address is expanded - assert "Expanded Address: 2001:db8:0000:0000:0000:0000:0000:0000" in result_text - # Verify CIDR prefix - assert "CIDR Prefix: /64" in result_text - # Verify network bits - assert "Network Bits: 64" in result_text - # Verify host bits - assert "Host Bits: 64" in result_text - - # CRITICAL: Verify network and broadcast addresses are calculated correctly - # For 2001:db8::/64, the network should be 2001:db8:: and broadcast should be 2001:db8::ffff:ffff:ffff:ffff - assert "Network Address: 2001:0db8:0000:0000:0000:0000:0000:0000" in result_text - assert "Broadcast Address: 2001:0db8:0000:0000:ffff:ffff:ffff:ffff" in result_text - - # NEW: Verify compressed address functionality is working - assert "Compressed Address:" in result_text, "Compressed address should be shown" - assert "Network Address (Compressed):" in result_text, "Compressed network address should be shown" - assert "Broadcast Address (Compressed):" in result_text, "Compressed broadcast address should be shown" - - # Verify the compressed address is actually compressed (shorter than expanded) - # The input was "2001:db8::" which should compress to "2001:db8::" - assert "Compressed Address: 2001:db8::" in result_text, "Should show compressed form of 2001:db8::" - - def test_subnet_ipv6_host_count_calculation(self, calculator_page): - """Test IPv6 host count calculations for different CIDR values""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # Switch to IPv6 - ip_version_select = calculator_page.find_element(By.CSS_SELECTOR, "select[name='ipVersion']") - calculator_page.execute_script("arguments[0].value = 'ipv6'; arguments[0].dispatchEvent(new Event('change'));", ip_version_select) - - # Wait for IPv6 inputs to appear - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipv6Address']")) - ) - - # Add a small delay to ensure inputs are fully ready - import time - time.sleep(0.5) - - # Force IPv6 mode since the event listener isn't working properly - calculator_page.execute_script(""" - document.getElementById('ipv4-inputs').style.display = 'none'; - document.getElementById('ipv6-inputs').style.display = 'block'; - const select = document.querySelector('select[name="ipVersion"]'); - select.value = 'ipv6'; - select.dispatchEvent(new Event('change', { bubbles: true })); - """) - - # Wait a moment for the mode switch to take effect - time.sleep(0.5) - - ipv6_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipv6Address']") - ipv6_cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipv6Cidr']") - - # Test /64 (standard IPv6 subnet) - # Use JavaScript to interact with inputs since they seem to have interaction issues - calculator_page.execute_script("arguments[0].value = '2001:db8::';", ipv6_input) - calculator_page.execute_script("arguments[0].value = '64';", ipv6_cidr_input) - - # Trigger the change events manually - calculator_page.execute_script("arguments[0].dispatchEvent(new Event('input', { bubbles: true }));", ipv6_input) - calculator_page.execute_script("arguments[0].dispatchEvent(new Event('input', { bubbles: true }));", ipv6_cidr_input) - - # Wait for results - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "result")) - ) - - result_text = self._get_subnet_result(calculator_page) - - # /64 should have 2^64 - 2 hosts - assert "Total Hosts:" in result_text - # Should show a large number (2^64 is approximately 1.84e+19) - assert "1.84e+19" in result_text or "18.4" in result_text - - # Test /48 (larger subnet) - ipv6_cidr_input.clear() - ipv6_cidr_input.send_keys("48") - - # Wait for results to update - WebDriverWait(calculator_page, 10).until( - lambda driver: "1.21e+24" in self._get_subnet_result(driver) or "1.21" in self._get_subnet_result(driver) - ) - - result_text = self._get_subnet_result(calculator_page) - # /48 should have 2^80 - 2 hosts - assert "1.21e+24" in result_text or "1.21" in result_text - - def test_subnet_ipv6_available_networks(self, calculator_page): - """Test IPv6 available networks calculation""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # Switch to IPv6 - ip_version_select = calculator_page.find_element(By.CSS_SELECTOR, "select[name='ipVersion']") - calculator_page.execute_script("arguments[0].value = 'ipv6'; arguments[0].dispatchEvent(new Event('change'));", ip_version_select) - - # Wait for IPv6 inputs to appear - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipv6Address']")) - ) - - # Force IPv6 mode since the event listener isn't working properly - calculator_page.execute_script(""" - document.getElementById('ipv4-inputs').style.display = 'none'; - document.getElementById('ipv6-inputs').style.display = 'block'; - const select = document.querySelector('select[name="ipVersion"]'); - select.value = 'ipv6'; - select.dispatchEvent(new Event('change', { bubbles: true })); - """) - - # Wait a moment for the mode switch to take effect - import time - time.sleep(0.5) - - ipv6_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipv6Address']") - ipv6_cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipv6Cidr']") - - # Test with /120 (smaller IPv6 subnet for manageable results) - # Use JavaScript to interact with inputs since they seem to have interaction issues - calculator_page.execute_script("arguments[0].value = '2001:db8::';", ipv6_input) - calculator_page.execute_script("arguments[0].value = '120';", ipv6_cidr_input) - - # Trigger the change events manually - calculator_page.execute_script("arguments[0].dispatchEvent(new Event('input', { bubbles: true }));", ipv6_input) - calculator_page.execute_script("arguments[0].dispatchEvent(new Event('input', { bubbles: true }));", ipv6_cidr_input) - - # Wait for results - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "result")) - ) - - result_text = self._get_subnet_result(calculator_page) - - # Should show available networks - assert "Available Networks" in result_text - # Should show network and broadcast addresses - assert "Network" in result_text - assert "Broadcast" in result_text - - def test_subnet_ipv4_ipv6_switching(self, calculator_page): - """Test switching between IPv4 and IPv6 modes""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # Initially should be IPv4 - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - assert ip_input.is_displayed() - - # Switch to IPv6 - ip_version_select = calculator_page.find_element(By.CSS_SELECTOR, "select[name='ipVersion']") - calculator_page.execute_script("arguments[0].value = 'ipv6'; arguments[0].dispatchEvent(new Event('change'));", ip_version_select) - - # Wait for IPv6 inputs to appear - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipv6Address']")) - ) - - ipv6_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipv6Address']") - assert ipv6_input.is_displayed() - - # IPv4 input should be hidden - assert not ip_input.is_displayed() - - # Switch back to IPv4 - calculator_page.execute_script("arguments[0].value = 'ipv4'; arguments[0].dispatchEvent(new Event('change'));", ip_version_select) - - # Wait for IPv4 inputs to appear - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # IPv4 input should be visible again - assert ip_input.is_displayed() - # IPv6 input should be hidden - assert not ipv6_input.is_displayed() - - def test_subnet_validation_errors(self, calculator_page): - """Test input validation and error handling""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='cidr']") - - # Test invalid IP address - ip_input.clear() - ip_input.send_keys("256.256.256.256") - cidr_input.clear() - cidr_input.send_keys("24") - - # Should not crash and should handle gracefully - # (The exact behavior depends on implementation) - - # Test invalid CIDR - ip_input.clear() - ip_input.send_keys("192.168.1.1") - cidr_input.clear() - cidr_input.send_keys("33") # Invalid CIDR for IPv4 - - # Should handle gracefully - - # Test edge case: CIDR 0 - cidr_input.clear() - cidr_input.send_keys("0") - - # Should handle gracefully - - def test_subnet_ipv6_compression_basic(self, calculator_page): - """Test basic IPv6 compression functionality""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - # Switch to IPv6 using the same method that works in the existing test - ip_version_select = calculator_page.find_element(By.CSS_SELECTOR, "select[name='ipVersion']") - calculator_page.execute_script("arguments[0].value = 'ipv6'; arguments[0].dispatchEvent(new Event('change'));", ip_version_select) - - # Wait for IPv6 inputs to appear - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipv6Address']")) - ) - - # Add a small delay to ensure inputs are fully ready - import time - time.sleep(0.5) - - # Force IPv6 mode using the same method that works - calculator_page.execute_script(""" - document.getElementById('ipv4-inputs').style.display = 'none'; - document.getElementById('ipv6-inputs').style.display = 'block'; - const select = document.querySelector('select[name="ipVersion"]'); - select.value = 'ipv6'; - select.dispatchEvent(new Event('change', { bubbles: true })); - """) - - # Wait a moment for the mode switch to take effect - time.sleep(0.5) - - ipv6_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipv6Address']") - ipv6_cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipv6Cidr']") - - # Test with a simple case that should compress well - # Use JavaScript to interact with inputs since they seem to have interaction issues - calculator_page.execute_script("arguments[0].value = '2001:db8:0000:0000:0000:0000:0000:0001';", ipv6_input) - calculator_page.execute_script("arguments[0].value = '64';", ipv6_cidr_input) - - # Trigger the change events manually - calculator_page.execute_script("arguments[0].dispatchEvent(new Event('input', { bubbles: true }));", ipv6_input) - calculator_page.execute_script("arguments[0].dispatchEvent(new Event('input', { bubbles: true }));", ipv6_cidr_input) - - # Wait for results - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "result")) - ) - - result_text = self._get_subnet_result(calculator_page) - - # Verify compression functionality is working - assert "Compressed Address:" in result_text, "Compressed address should be shown" - assert "Network Address (Compressed):" in result_text, "Compressed network address should be shown" - assert "Broadcast Address (Compressed):" in result_text, "Compressed broadcast address should be shown" - - # Verify the compressed address is actually compressed - # Input: 2001:db8:0000:0000:0000:0000:0000:0001 should compress to 2001:db8::1 - assert "Compressed Address: 2001:db8::1" in result_text, "Should show compressed form 2001:db8::1" - - # Verify the table shows both expanded and compressed columns - assert "Network (Expanded)" in result_text, "Table should show expanded network column" - assert "Network (Compressed)" in result_text, "Table should show compressed network column" - assert "Broadcast (Expanded)" in result_text, "Table should show expanded broadcast column" - assert "Broadcast (Compressed)" in result_text, "Table should show compressed broadcast column" - - def test_subnet_ipv4_network_class_edge_cases(self, calculator_page): - """Test IPv4 network class detection for all classes and edge cases""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - ip_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='ipAddress']") - cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='cidr']") - - # Test all network classes and edge cases - test_cases = [ - ("1.0.0.1", "Class A"), # Class A start - ("126.255.255.255", "Class A"), # Class A end - ("128.0.0.1", "Class B"), # Class B start - ("191.255.255.255", "Class B"), # Class B end - ("192.0.0.1", "Class C"), # Class C start - ("223.255.255.255", "Class C"), # Class C end - ("224.0.0.1", "Class D"), # Class D start (multicast) - ("239.255.255.255", "Class D"), # Class D end - ("240.0.0.1", "Class E"), # Class E start (experimental) - ("255.255.255.255", "Class E"), # Class E end - ("0.0.0.0", "Class A"), # Edge case: 0.0.0.0 - ("127.0.0.1", "Class A"), # Edge case: loopback - ] - - for ip_addr, expected_class in test_cases: - # Set the input - ip_input.clear() - ip_input.send_keys(ip_addr) - cidr_input.clear() - cidr_input.send_keys("24") - - # Wait for results - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "result")) - ) - - result_text = self._get_subnet_result(calculator_page) - - # Verify network class is correct - assert f"Network Class: {expected_class}" in result_text, f"Failed for {ip_addr}: expected {expected_class}" - - def test_subnet_cidr_mask_conversion_edge_cases(self, calculator_page): - """Test CIDR to mask conversion for all edge cases""" - calculator_page.get("http://localhost:8008/subnet") - - # Wait for calculator to load - WebDriverWait(calculator_page, 10).until( - EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='ipAddress']")) - ) - - cidr_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='cidr']") - subnet_mask_input = calculator_page.find_element(By.CSS_SELECTOR, "input[name='subnetMask']") - - # Test all CIDR values and their corresponding masks - test_cases = [ - (0, "0.0.0.0"), - (1, "128.0.0.0"), - (8, "255.0.0.0"), - (16, "255.255.0.0"), - (24, "255.255.255.0"), - (25, "255.255.255.128"), - (30, "255.255.255.252"), - (31, "255.255.255.254"), - (32, "255.255.255.255"), - ] - - for cidr, expected_mask in test_cases: - # Set CIDR value - cidr_input.clear() - cidr_input.send_keys(str(cidr)) - - # Wait for subnet mask to update - WebDriverWait(calculator_page, 10).until( - lambda driver: subnet_mask_input.get_attribute("value") == expected_mask - ) - - # Verify the mask is correct - actual_mask = subnet_mask_input.get_attribute("value") - assert actual_mask == expected_mask, f"CIDR /{cidr} should map to {expected_mask}, got {actual_mask}" - - # Also test reverse conversion (mask to CIDR) - subnet_mask_input.clear() - subnet_mask_input.send_keys(expected_mask) - - # Wait for CIDR to update - WebDriverWait(calculator_page, 10).until( - lambda driver: cidr_input.get_attribute("value") == str(cidr) - ) - - # Verify the CIDR is correct - actual_cidr = cidr_input.get_attribute("value") - assert actual_cidr == str(cidr), f"Mask {expected_mask} should map to /{cidr}, got /{actual_cidr}" - - def _get_subnet_result(self, driver): - """Helper method to get subnet calculation result text""" - result_element = driver.find_element(By.CLASS_NAME, "result") - return result_element.text diff --git a/tests/test_zfs.py b/tests/test_zfs.py deleted file mode 100644 index 9a3a4c3..0000000 --- a/tests/test_zfs.py +++ /dev/null @@ -1,1028 +0,0 @@ -import pytest -from selenium.webdriver.common.by import By -from selenium.webdriver.support.ui import WebDriverWait -from selenium.webdriver.support import expected_conditions as EC -from selenium.webdriver.common.keys import Keys -import time - -@pytest.fixture -def zfs_page(driver, dev_server): - """Navigate to ZFS calculator page""" - driver.get(f"{dev_server}") - - # Wait for page to load and JavaScript to populate navigation - WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.CLASS_NAME, "sidenav")) - ) - # Wait for JavaScript to populate the navigation with calculator links - WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.XPATH, "//a[contains(text(), 'ZFS')]")) - ) - - # Click on ZFS Calculator - zfs_btn = driver.find_element(By.XPATH, "//a[contains(text(), 'ZFS')]") - zfs_btn.click() - - # Wait for ZFS calculator to load - WebDriverWait(driver, 10).until( - EC.presence_of_element_located((By.NAME, "poolType")) - ) - - return driver - -def _get_zfs_result(zfs_page): - """Helper to get the correct ZFS result element""" - result_elements = zfs_page.find_elements(By.CLASS_NAME, "result") - - # Look for the result element that contains ZFS-related content - zfs_result = None - for elem in result_elements: - html = elem.get_attribute('innerHTML') - if 'ZFS' in html or 'Pool Configuration' in html or 'Capacity' in html: - zfs_result = elem - break - - if not zfs_result: - # If no ZFS result found, use the last result element (most recent) - zfs_result = result_elements[-1] - - return zfs_result - -def test_zfs_calculator_loads(zfs_page): - """Test that the ZFS calculator loads correctly""" - # Wait a bit for JavaScript to fully load - time.sleep(2) - - # Check for key elements - assert zfs_page.find_element(By.NAME, "poolType") - assert zfs_page.find_element(By.NAME, "diskCount") - assert zfs_page.find_element(By.NAME, "diskSize") - assert zfs_page.find_element(By.NAME, "compression") - - # Find the displayed result element (should be the ZFS one) - displayed_result = None - result_elements = zfs_page.find_elements(By.CLASS_NAME, "result") - for elem in result_elements: - if elem.is_displayed(): - displayed_result = elem - break - - assert displayed_result is not None, "No displayed result element found" - - # Check that the ZFS result is shown - assert "ZFS Pool Configuration" in displayed_result.text - -def test_zfs_pool_type_selection(zfs_page): - """Test that pool type selection works and updates calculations""" - # Get initial result - initial_result = _get_zfs_result(zfs_page) - initial_text = initial_result.text - - # Change pool type to mirror using Select Lite - pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') - pool_type_button.click() - - # Wait for dropdown to appear and click the mirror option - mirror_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror')]")) - ) - mirror_option.click() - - # Wait for calculation to update - time.sleep(1) - - # Check that result changed - updated_result = _get_zfs_result(zfs_page) - updated_text = updated_result.text - assert updated_text != initial_text - - # Check that the calculation reflects mirror configuration (50% efficiency for 2-way mirror) - assert "50.0%" in updated_text - assert "12000.0 GB" in updated_text # Half of 24000 GB for 2-way mirror - -def test_zfs_disk_count_validation(zfs_page): - """Test that disk count validation works correctly""" - # Set pool type to RAID-Z2 (requires at least 4 disks) using Select Lite - pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') - pool_type_button.click() - - # Wait for dropdown to appear and click the RAID-Z2 option - raidz2_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]")) - ) - raidz2_option.click() - - # Set disk count to 2 (invalid for RAID-Z2) - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys("2") - - # Wait for validation error - time.sleep(1) - - # Check for error message - result = _get_zfs_result(zfs_page) - assert "Error" in result.text - assert "RAIDZ2 requires at least 4 disks" in result.text - -def test_zfs_compression_calculation(zfs_page): - """Test that compression ratios are calculated correctly""" - # Set compression to LZ4 using Select Lite - compression_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="compression"] + .select-lite__button') - compression_button.click() - - # Wait for dropdown to appear and click the LZ4 option - lz4_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'LZ4')]")) - ) - lz4_option.click() - - # Wait for calculation - time.sleep(1) - - # Check that LZ4 compression ratio is shown - result = _get_zfs_result(zfs_page) - assert "2.1x ratio" in result.text - -def test_zfs_disk_size_unit_conversion(zfs_page): - """Test that disk size unit conversion works""" - # Get initial result with GB - initial_result = _get_zfs_result(zfs_page) - initial_text = initial_result.text - - # Change to TB using Select Lite - disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') - disk_size_unit_button.click() - - # Wait for dropdown to appear and click the TB option - tb_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) - ) - tb_option.click() - - # Wait for calculation - time.sleep(1) - - # Check that result changed (TB should show much larger numbers) - updated_result = _get_zfs_result(zfs_page) - updated_text = updated_result.text - assert updated_text != initial_text - -def test_zfs_block_size_selection(zfs_page): - """Test that block size selection works""" - # Change block size to 1M using Select Lite - block_size_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="blockSize"] + .select-lite__button') - block_size_button.click() - - # Wait for dropdown to appear and click the 1M option - one_mb_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), '1M')]")) - ) - one_mb_option.click() - - # Wait for calculation - time.sleep(1) - - # Check that 1M block size is shown in results - result = _get_zfs_result(zfs_page) - assert "1M" in result.text - -def test_zfs_ashift_calculation(zfs_page): - """Test that ashift values are calculated correctly""" - # Set ashift to 13 using Select Lite - ashift_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="ashift"] + .select-lite__button') - ashift_button.click() - - # Wait for dropdown to appear and click the ashift 13 option - ashift_13_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), '13 (8KB sectors)')]")) - ) - ashift_13_option.click() - - # Wait for calculation - time.sleep(1) - - # Check that ashift 13 and 8KB are shown - result = _get_zfs_result(zfs_page) - assert "13 (8192 bytes)" in result.text - -def test_zfs_arc_max_input(zfs_page): - """Test that ARC max size input works""" - # Change ARC max to 16384 MB - arc_max_input = zfs_page.find_element(By.NAME, "arcMax") - arc_max_input.clear() - arc_max_input.send_keys("16384") - - # Wait for calculation - time.sleep(1) - - # Check that 16384 MB is shown in results - result = _get_zfs_result(zfs_page) - assert "16384 MB" in result.text - -def test_zfs_dedup_warning(zfs_page): - """Test that deduplication warning is shown when enabled""" - # Enable deduplication using Select Lite - dedup_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="dedup"] + .select-lite__button') - dedup_button.click() - - # Wait for dropdown to appear and click the dedup on option - dedup_on_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'On (Use with Caution)')]")) - ) - dedup_on_option.click() - - # Wait for calculation - time.sleep(1) - - # Check that calculator still works with dedup enabled - result = _get_zfs_result(zfs_page) - assert "ZFS Pool Configuration" in result.text - -def test_zfs_recommendations(zfs_page): - """Test that core calculator functionality works""" - # Check that core sections exist - result = _get_zfs_result(zfs_page) - assert "ZFS Pool Configuration" in result.text - assert "Capacity & Redundancy" in result.text - assert "Performance Settings" in result.text - -def test_zfs_capacity_calculations(zfs_page): - """Test that capacity calculations are accurate""" - # Set to RAID-Z2 with 6 disks of 4TB each using Select Lite - pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') - pool_type_button.click() - - # Wait for dropdown to appear and click the RAID-Z2 option - raidz2_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]")) - ) - raidz2_option.click() - - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys("6") - - disk_size_input = zfs_page.find_element(By.NAME, "diskSize") - disk_size_input.clear() - disk_size_input.send_keys("4") - - # Change to TB using Select Lite - disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') - disk_size_unit_button.click() - - # Wait for dropdown to appear and click the TB option - tb_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) - ) - tb_option.click() - - # Wait for calculation - time.sleep(1) - - # Check capacity calculations - result = _get_zfs_result(zfs_page) - result_text = result.text - - # 6 disks * 4TB = 24TB raw capacity - assert "24.0 TB" in result_text or "24576.0 GB" in result_text - - # RAID-Z2 with 6 disks: (6-2) * 4TB = 16TB usable - assert "16.0 TB" in result_text or "16384.0 GB" in result_text - - # Efficiency should be 66.7% (16/24) - assert "66.7%" in result_text - -def test_zfs_new_pool_types(zfs_page): - """Test the new pool types (mirror2x2, mirror3x2, raidz2x2)""" - # Test mirror2x2 - pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') - pool_type_button.click() - - # Wait for dropdown and select mirror2x2 - mirror2x2_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 2x2')]")) - ) - mirror2x2_option.click() - - # Set disk count to 4 - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys("4") - - # Wait for calculation - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Mirror2x2 with 4 disks should have ~8TB usable (2 mirrors of 4TB each, default size) - assert "7.81 TB" in result_text or "8000.0 GB" in result_text - assert "VDev Count: 2 vdevs" in result_text - -def test_zfs_performance_estimates(zfs_page): - """Test that performance estimates are displayed""" - # Use default settings - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Should show performance estimates - assert "Performance Estimates" in result_text - assert "Estimated IOPS:" in result_text - assert "Estimated Throughput:" in result_text - assert "Recommended RAM:" in result_text - assert "System Requirements" in result_text - assert "Minimum RAM:" in result_text - assert "CPU Cores:" in result_text - assert "Network:" in result_text - - - -def test_zfs_capacity_formatting(zfs_page): - """Test that capacity is formatted with both GB and TB""" - # Set to a large size to trigger TB formatting - disk_size_input = zfs_page.find_element(By.NAME, "diskSize") - disk_size_input.clear() - disk_size_input.send_keys("2") - - # Change to TB - disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') - disk_size_unit_button.click() - - tb_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) - ) - tb_option.click() - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Should show both GB and TB for large capacities - assert "GB (" in result_text and "TB)" in result_text - assert "Compression Savings:" in result_text - -def test_zfs_all_pool_types(zfs_page): - """Test all pool types for correct capacity calculations""" - pool_types = [ - ('stripe', 1, 'None', 1, 1), - ('mirror', 2, '50%', 2, 2), - ('raidz1', 3, '1 disk', 3, 3), - ('raidz2', 4, '2 disks', 4, 6), - ('raidz3', 5, '3 disks', 5, 9), - ('mirror2x2', 4, '50%', 4, 4), - ('mirror3x2', 6, '50%', 6, 6), - ('raidz2x2', 8, '2 disks per vdev', 8, 12) - ] - - for pool_type, min_disks, redundancy, recommended_min, recommended_optimal in pool_types: - # Select pool type - pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') - pool_type_button.click() - - # Find and click the specific pool type option - if pool_type == 'stripe': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Stripe')]" - elif pool_type == 'mirror': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror (2-way)')]" - elif pool_type == 'raidz1': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z1')]" - elif pool_type == 'raidz2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]" - elif pool_type == 'raidz3': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z3')]" - elif pool_type == 'mirror2x2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 2x2')]" - elif pool_type == 'mirror3x2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 3x2')]" - elif pool_type == 'raidz2x2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2 2x2')]" - - pool_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, xpath)) - ) - pool_option.click() - - # Set appropriate disk count - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys(str(min_disks)) - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Verify redundancy is correct - assert redundancy in result_text, f"Pool type {pool_type} should show redundancy: {redundancy}" - - # Verify no error for minimum disk count - assert "Error" not in result_text, f"Pool type {pool_type} should not show error with {min_disks} disks" - -def test_zfs_compression_algorithms(zfs_page): - """Test all compression algorithms for correct ratios""" - compression_tests = [ - ('off', '1.0x ratio'), - ('lz4', '2.1x ratio'), - ('gzip', '2.5x ratio'), - ('gzip-1', '2.0x ratio'), - ('gzip-9', '3.0x ratio'), - ('zstd', '2.8x ratio'), - ('zstd-1', '2.2x ratio'), - ('zstd-19', '3.5x ratio') - ] - - for compression, expected_ratio in compression_tests: - # Select compression algorithm - compression_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="compression"] + .select-lite__button') - compression_button.click() - - # Find and click the specific compression option - if compression == 'off': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Off')]" - elif compression == 'lz4': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'LZ4')]" - elif compression == 'gzip': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Gzip (Balanced)')]" - elif compression == 'gzip-1': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Gzip-1')]" - elif compression == 'gzip-9': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Gzip-9')]" - elif compression == 'zstd': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Zstd (Modern)')]" - elif compression == 'zstd-1': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Zstd-1')]" - elif compression == 'zstd-19': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Zstd-19')]" - - compression_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, xpath)) - ) - compression_option.click() - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Verify compression ratio is shown - assert expected_ratio in result_text, f"Compression {compression} should show ratio: {expected_ratio}" - -def test_zfs_block_sizes(zfs_page): - """Test all block sizes are properly displayed""" - block_sizes = ['4K', '8K', '16K', '32K', '64K', '128K', '256K', '512K', '1M'] - - for block_size in block_sizes: - # Select block size - block_size_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="blockSize"] + .select-lite__button') - block_size_button.click() - - # Find and click the specific block size option - block_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, f"//div[contains(@class, 'select-lite__option') and contains(text(), '{block_size}')]")) - ) - block_option.click() - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Verify block size is shown in results - assert block_size in result_text, f"Block size {block_size} should be displayed in results" - -def test_zfs_ashift_values(zfs_page): - """Test all ashift values and their byte calculations""" - ashift_tests = [ - (9, '512 bytes'), - (12, '4096 bytes'), - (13, '8192 bytes'), - (14, '16384 bytes') - ] - - for ashift, expected_bytes in ashift_tests: - # Select ashift value - ashift_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="ashift"] + .select-lite__button') - ashift_button.click() - - # Find and click the specific ashift option - if ashift == 9: - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '9 (512B sectors)')]" - elif ashift == 12: - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '12 (4KB sectors)')]" - elif ashift == 13: - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '13 (8KB sectors)')]" - elif ashift == 14: - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '14 (16KB sectors)')]" - - ashift_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, xpath)) - ) - ashift_option.click() - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Verify ashift and byte calculation are shown - assert f"{ashift} ({expected_bytes})" in result_text, f"Ashift {ashift} should show {expected_bytes}" - -def test_zfs_arc_max_units(zfs_page): - """Test ARC max size with MB unit""" - # Test MB unit (default) - arc_max_input = zfs_page.find_element(By.NAME, "arcMax") - arc_max_input.clear() - arc_max_input.send_keys("8192") - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - assert "8192 MB" in result_text, "ARC max should show MB unit" - - # Test different MB value - arc_max_input.clear() - arc_max_input.send_keys("16384") - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - assert "16384 MB" in result_text, "ARC max should update when changed" - -def test_zfs_disk_count_validation_all_types(zfs_page): - """Test disk count validation for all pool types""" - validation_tests = [ - ('stripe', 0, False), # stripe can have 1+ disks - ('stripe', 1, True), # stripe minimum - ('mirror', 1, False), # mirror needs 2+ disks - ('mirror', 2, True), # mirror minimum - ('raidz1', 2, False), # raidz1 needs 3+ disks - ('raidz1', 3, True), # raidz1 minimum - ('raidz2', 3, False), # raidz2 needs 4+ disks - ('raidz2', 4, True), # raidz2 minimum - ('raidz3', 4, False), # raidz3 needs 5+ disks - ('raidz3', 5, True), # raidz3 minimum - ('mirror2x2', 3, False), # mirror2x2 needs 4 disks - ('mirror2x2', 4, True), # mirror2x2 minimum - ('mirror3x2', 5, False), # mirror3x2 needs 6 disks - ('mirror3x2', 6, True), # mirror3x2 minimum - ('raidz2x2', 7, False), # raidz2x2 needs 8+ disks - ('raidz2x2', 8, True), # raidz2x2 minimum - ] - - for pool_type, disk_count, should_be_valid in validation_tests: - # Select pool type - pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') - pool_type_button.click() - - # Find and click the specific pool type option - if pool_type == 'stripe': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Stripe')]" - elif pool_type == 'mirror': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror (2-way)')]" - elif pool_type == 'raidz1': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z1')]" - elif pool_type == 'raidz2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]" - elif pool_type == 'raidz3': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z3')]" - elif pool_type == 'mirror2x2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 2x2')]" - elif pool_type == 'mirror3x2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 3x2')]" - elif pool_type == 'raidz2x2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2 2x2')]" - - pool_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, xpath)) - ) - pool_option.click() - - # Set disk count - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys(str(disk_count)) - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - if should_be_valid: - assert "Error" not in result_text, f"Pool type {pool_type} with {disk_count} disks should be valid" - else: - assert "Error" in result_text, f"Pool type {pool_type} with {disk_count} disks should show error" - -def test_zfs_performance_calculations_accuracy(zfs_page): - """Test that performance calculations are mathematically correct""" - # Set up a known configuration - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys("6") - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Extract and verify IOPS calculation (should be 6 * 100 = 600) - assert "600" in result_text, "IOPS should be 600 for 6 disks (6 * 100)" - - # Extract and verify throughput calculation (should be 6 * 150 = 900) - assert "900 MB/s" in result_text, "Throughput should be 900 MB/s for 6 disks (6 * 150)" - - # Verify RAM recommendation (should be 6 * 4TB / 1024 = ~24GB minimum) - assert "24 GB minimum" in result_text, "RAM recommendation should be ~24GB for 24TB storage" - - - -def test_zfs_edge_cases_and_validation(zfs_page): - """Test edge cases and input validation""" - # Test negative disk count - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys("-1") - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Should handle negative gracefully (likely clamp to 1 or show error) - assert "Error" in result_text or "1" in result_text, "Should handle negative disk count" - - # Test zero disk count - disk_count_input.clear() - disk_count_input.send_keys("0") - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - assert "Error" in result_text, "Should show error for zero disk count" - - # Test very large disk count - disk_count_input.clear() - disk_count_input.send_keys("100") - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Should handle large numbers gracefully - assert "Error" not in result_text, "Should handle large disk count gracefully" - - # Test negative disk size - disk_size_input = zfs_page.find_element(By.NAME, "diskSize") - disk_size_input.clear() - disk_size_input.send_keys("-1") - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Should handle negative disk size gracefully - assert "Error" in result_text or "0" in result_text, "Should handle negative disk size" - -def test_zfs_state_persistence(zfs_page): - """Test that calculator state persists across page reloads""" - # Change some settings - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys("8") - - disk_size_input = zfs_page.find_element(By.NAME, "diskSize") - disk_size_input.clear() - disk_size_input.send_keys("2") - - # Change to TB - disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') - disk_size_unit_button.click() - - tb_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) - ) - tb_option.click() - - time.sleep(1) - - # Reload the page - zfs_page.refresh() - - # Wait for page to reload - WebDriverWait(zfs_page, 10).until( - EC.presence_of_element_located((By.NAME, "poolType")) - ) - - time.sleep(2) - - # Check if settings were restored - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_size_input = zfs_page.find_element(By.NAME, "diskSize") - - # Values should be restored (exact values may vary due to browser behavior) - assert disk_count_input.get_attribute('value') in ['8', '6'], "Disk count should be restored" - assert disk_size_input.get_attribute('value') in ['2', '4'], "Disk size should be restored" - -def test_zfs_compression_savings_calculation(zfs_page): - """Test ZFS-specific compression savings calculation""" - # Set up RAID-Z2 with 6 disks, 4TB each - pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') - pool_type_button.click() - - raidz2_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]")) - ) - raidz2_option.click() - - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys("6") - - disk_size_input = zfs_page.find_element(By.NAME, "diskSize") - disk_size_input.clear() - disk_size_input.send_keys("4") - - # Change to TB - disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') - disk_size_unit_button.click() - - tb_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) - ) - tb_option.click() - - # Test LZ4 compression (2.1x ratio) - compression_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="compression"] + .select-lite__button') - compression_button.click() - - lz4_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'LZ4')]")) - ) - lz4_option.click() - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # RAID-Z2 with 6 disks: (6-2) * 4TB = 16TB usable - # With LZ4 (2.1x): 16TB * 2.1 = 33.6TB effective - # Compression savings: (33.6 - 16) / 16 * 100 = 110% - assert "16.00 TB" in result_text, "Usable capacity should be 16TB" - assert "33.60 TB" in result_text, "Effective capacity should be 33.6TB with LZ4" - assert "110.0%" in result_text, "Compression savings should be 110%" - -def test_zfs_efficiency_calculation(zfs_page): - """Test ZFS-specific efficiency calculation""" - # Test different pool types for efficiency - efficiency_tests = [ - ('stripe', 1, '100.0%'), # 100% efficiency - ('mirror', 2, '50.0%'), # 50% efficiency - ('raidz1', 3, '66.7%'), # 66.7% efficiency - ('raidz2', 6, '66.7%'), # 66.7% efficiency - ] - - for pool_type, disk_count, expected_efficiency in efficiency_tests: - # Select pool type - pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') - pool_type_button.click() - - if pool_type == 'stripe': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Stripe')]" - elif pool_type == 'mirror': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror (2-way)')]" - elif pool_type == 'raidz1': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z1')]" - elif pool_type == 'raidz2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]" - - pool_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, xpath)) - ) - pool_option.click() - - # Set disk count - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys(str(disk_count)) - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - assert expected_efficiency in result_text, f"Pool type {pool_type} with {disk_count} disks should have {expected_efficiency} efficiency" - -def test_zfs_vdev_count_logic(zfs_page): - """Test ZFS-specific vdev count calculations""" - vdev_tests = [ - ('stripe', 1, '1 vdev'), - ('mirror', 2, '1 vdev'), - ('mirror', 4, '2 vdevs'), - ('raidz1', 3, '1 vdev'), - ('raidz2', 6, '1 vdev'), - ('mirror2x2', 4, '2 vdevs'), - ('mirror3x2', 6, '3 vdevs'), - ('raidz2x2', 8, '2 vdevs'), - ] - - for pool_type, disk_count, expected_vdevs in vdev_tests: - # Select pool type - pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') - pool_type_button.click() - - if pool_type == 'stripe': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Stripe')]" - elif pool_type == 'mirror': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror (2-way)')]" - elif pool_type == 'raidz1': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z1')]" - elif pool_type == 'raidz2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2')]" - elif pool_type == 'mirror2x2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 2x2')]" - elif pool_type == 'mirror3x2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror 3x2')]" - elif pool_type == 'raidz2x2': - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2 2x2')]" - - pool_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, xpath)) - ) - pool_option.click() - - # Set disk count - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys(str(disk_count)) - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - assert expected_vdevs in result_text, f"Pool type {pool_type} with {disk_count} disks should have {expected_vdevs}" - -def test_zfs_block_size_parsing(zfs_page): - """Test ZFS-specific block size parsing and display""" - block_size_tests = [ - ('4K', '4K'), - ('128K', '128K'), - ('1M', '1M'), - ] - - for block_size, expected_display in block_size_tests: - # Select block size - block_size_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="blockSize"] + .select-lite__button') - block_size_button.click() - - block_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, f"//div[contains(@class, 'select-lite__option') and contains(text(), '{block_size}')]")) - ) - block_option.click() - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - assert f"Block Size: {expected_display}" in result_text, f"Block size {block_size} should display as {expected_display}" - -def test_zfs_ashift_byte_calculation(zfs_page): - """Test ZFS-specific ashift to byte conversion""" - ashift_byte_tests = [ - (9, '512 bytes'), - (12, '4096 bytes'), - (13, '8192 bytes'), - (14, '16384 bytes') - ] - - for ashift, expected_bytes in ashift_byte_tests: - # Select ashift value - ashift_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="ashift"] + .select-lite__button') - ashift_button.click() - - if ashift == 9: - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '9 (512B sectors)')]" - elif ashift == 12: - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '12 (4KB sectors)')]" - elif ashift == 13: - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '13 (8KB sectors)')]" - elif ashift == 14: - xpath = "//div[contains(@class, 'select-lite__option') and contains(text(), '14 (16KB sectors)')]" - - ashift_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, xpath)) - ) - ashift_option.click() - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # Verify the mathematical calculation: 2^ashift = expected_bytes - expected_calculation = 2 ** ashift - assert f"{ashift} ({expected_calculation} bytes)" in result_text, f"Ashift {ashift} should calculate to {expected_calculation} bytes" - -def test_zfs_raidz2x2_complex_calculation(zfs_page): - """Test the complex RAID-Z2 2x2 calculation: 2 * (diskCount / 2 - 2) * diskSizeGB""" - # Select RAID-Z2 2x2 - pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') - pool_type_button.click() - - raidz2x2_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'RAID-Z2 2x2')]")) - ) - raidz2x2_option.click() - - # Test with 8 disks, 2TB each - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys("8") - - disk_size_input = zfs_page.find_element(By.NAME, "diskSize") - disk_size_input.clear() - disk_size_input.send_keys("2") - - # Change to TB - disk_size_unit_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="diskSizeUnit"] + .select-lite__button') - disk_size_unit_button.click() - - tb_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'TB')]")) - ) - tb_option.click() - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # RAID-Z2 2x2 with 8 disks, 2TB each: - # Formula: 2 * (8/2 - 2) * 2TB = 2 * (4 - 2) * 2TB = 2 * 2 * 2TB = 8TB - assert "8.00 TB" in result_text, "RAID-Z2 2x2 with 8 disks of 2TB should have 8TB usable capacity" - -def test_zfs_mirror_vdev_count_calculation(zfs_page): - """Test ZFS-specific mirror vdev count: Math.floor(diskCount / 2)""" - # Test different mirror configurations - mirror_tests = [ - (2, '1 vdev'), # 2 disks = 1 mirror vdev - (4, '2 vdevs'), # 4 disks = 2 mirror vdevs - (6, '3 vdevs'), # 6 disks = 3 mirror vdevs - (8, '4 vdevs'), # 8 disks = 4 mirror vdevs - ] - - for disk_count, expected_vdevs in mirror_tests: - # Select mirror pool type - pool_type_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="poolType"] + .select-lite__button') - pool_type_button.click() - - mirror_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'Mirror (2-way)')]")) - ) - mirror_option.click() - - # Set disk count - disk_count_input = zfs_page.find_element(By.NAME, "diskCount") - disk_count_input.clear() - disk_count_input.send_keys(str(disk_count)) - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - assert expected_vdevs in result_text, f"Mirror with {disk_count} disks should have {expected_vdevs}" - -def test_zfs_compression_ratio_edge_cases(zfs_page): - """Test ZFS compression ratio edge cases""" - # Test with compression off (1.0x ratio) - compression_button = zfs_page.find_element(By.CSS_SELECTOR, '[name="compression"] + .select-lite__button') - compression_button.click() - - off_option = WebDriverWait(zfs_page, 10).until( - EC.element_to_be_clickable((By.XPATH, "//div[contains(@class, 'select-lite__option') and contains(text(), 'Off')]")) - ) - off_option.click() - - time.sleep(1) - - result = _get_zfs_result(zfs_page) - result_text = result.text - - # With compression off, effective capacity should equal usable capacity - # Compression savings should be 0% - assert "1.0x ratio" in result_text, "Compression off should show 1.0x ratio" - assert "0.0%" in result_text, "Compression savings should be 0% with compression off"