diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f4237c6..8a52514 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,7 +24,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pytest + pip install -r requirements.txt - name: Run tests run: pytest -v diff --git a/CHANGELOG.md b/CHANGELOG.md index 6afb671..69c943d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,152 @@ y este proyecto se adhiere a [Semantic Versioning](https://semver.org/spec/v2.0. --- +## [2.1.0] - 2025-12-03 + +### 🎉 Minor Release - Operaciones con Paréntesis + Reorganización de Proyecto + +Este release introduce soporte completo para expresiones matemáticas con paréntesis, reorganiza la estructura del proyecto siguiendo mejores prácticas de Python, y corrige bugs importantes relacionados con números complejos y decimales negativos. + +### Agregado + +#### Operaciones con Paréntesis (#44, #56) + +- **Soporte completo de paréntesis** en la calculadora GUI + - Evaluación de expresiones matemáticas complejas: `(2+3)*4`, `2*(3+4)`, `((2+3)*4)/5` + - Botones funcionales `(` y `)` en la interfaz + - Entrada de paréntesis desde teclado + - Validación de paréntesis balanceados + - Validación de caracteres seguros con regex + - Modo de expresión automático al usar paréntesis + - Compatibilidad con modo normal (sin paréntesis) + - Soporte para decimales y números negativos en expresiones + - Paréntesis anidados soportados + +#### Testing de Paréntesis + +- **13 tests unitarios nuevos** para funcionalidad de paréntesis (#56) + - Tests de operaciones básicas con paréntesis + - Tests de paréntesis anidados + - Tests con decimales y números negativos + - Tests con potencias + - Tests de validación de paréntesis desbalanceados + - Tests de modo de expresión + - Tests de compatibilidad entre modos + - Tests de división por cero en expresiones + - Tests de actualización del display + +#### Reorganización del Proyecto (#53, #54) + +- **Nueva estructura de carpetas** profesional + - Carpeta `src/` para código fuente + - `src/calculator.py` - Lógica matemática + - `src/cli.py` - Interfaz de línea de comandos (renombrado desde main.py) + - `src/gui.py` - Interfaz gráfica + - `src/__init__.py` - Paquete Python + - Carpeta `tests/` para tests + - `tests/test_calculator.py` + - `tests/test_gui.py` (renombrado desde test_gui_calculator.py) + - `tests/conftest.py` + - `tests/__init__.py` + - Archivo `requirements.txt` para gestión de dependencias +- **Mejor escalabilidad** y organización del código +- **Imports mejorados** con estructura de paquetes +- **Comandos actualizados**: `python src/gui.py`, `python src/cli.py` + +### Corregido + +#### Raíces Pares de Números Negativos (#50, #57) + +- **Fix: Evitar resultados complejos** en raíces pares negativas + - Validación en `calculator.py` para detectar raíces pares de negativos + - Lanza `ValueError` con mensaje "Raíz negativa" + - Captura del error en GUI con mensaje claro "⚠️ Raíz negativa" + - Tests unitarios para verificar el comportamiento + - Ejemplo: `-2 ^ 0.5` ahora muestra error en lugar de `1.414j` + - Algoritmo: valida si `1/exponent` es par para detectar raíces pares + +#### Manejo de Decimales Negativos (#49, #55) + +- **Mejora de UX** en entrada de decimales negativos + - Autocompletado de `-0.` al presionar `.` después de `-` + - Validación mejorada para evitar números incompletos como `-.` + - Comportamiento consistente: `"-"` + `"."` → `"-0."` + - Ejemplo: `-.3` ahora se autocompleta a `-0.3` + - Mejor experiencia de usuario y consistencia + +### Mejorado + +- **Evaluación de expresiones** con validación de seguridad usando regex +- **Manejo de errores** más robusto con mensajes específicos +- **Display de GUI** actualizado correctamente en modo expresión +- **Backspace** funciona en ambos modos (normal y expresión) +- **Clear** resetea correctamente el modo de expresión +- **Estructura del proyecto** más profesional y escalable +- **Imports** más claros y mantenibles + +### Técnico + +**Paréntesis:** + +- Implementación de `open_parenthesis_click()` y `close_parenthesis_click()` +- Variable `expression` para construir expresiones completas +- Variable `use_expression_mode` para detectar uso de paréntesis +- Validación con regex `^[\d\s\+\-\*\/\^\(\)\.]+$` +- Conversión de `^` a `**` para evaluación de potencias +- Evaluación segura con `eval()` después de validaciones +- Manejo de excepciones: `SyntaxError`, `ZeroDivisionError` + +**Reorganización:** + +- Migración de archivos a `src/` y `tests/` +- Actualización de imports relativos +- Creación de `__init__.py` en paquetes +- `requirements.txt` con `pytest>=7.0.0` + +**Validaciones:** + +- Detección de raíces pares con algoritmo de inversión de exponente +- Validación de decimales negativos en `decimal_click()` + +### Issues y PRs Incluidas + +**Issues Completadas:** + +- #44 - Soporte de operaciones con paréntesis en la calculadora GUI +- #50 - Error: Raíces pares de números negativos generan resultados complejos +- #49 - Error: Manejo confuso de números decimales negativos +- #53 - Mejora: reorganizar estructura del proyecto + +**Pull Requests Mergeados:** + +- #57 - fix: evitar resultados complejos para raíces pares negativas (2025-12-03) +- #56 - feat: implementar soporte de paréntesis en calculadora (2025-12-02) +- #55 - Fix: Corrección del comportamiento del decimal después del signo menos (2025-12-01) +- #54 - refactor: reorganizar estructura del proyecto (src/, tests/, requirements.txt) (2025-11-30) + +### Agradecimientos + +Este release fue posible gracias a las contribuciones de: + +- **@Jandres25** (Jose Andres Meneces Lopez) + + - Implementación de soporte de paréntesis (#56) + - 13 tests unitarios para paréntesis + - Reorganización de estructura del proyecto (#54) + - Coordinación del release v2.1.0 + +- **@Jhos3ph** + + - Fix de raíces pares negativas (#57) + - Validación de números complejos + - Tests unitarios para validación + +- **@alexricardotapiacarita-ai** + - Fix de manejo de decimales negativos (#55) + - Mejoras de UX en entrada de números + +--- + ## [2.0.0] - 2025-11-28 ### 🎉 Major Release - Interfaz Gráfica + Testing Automatizado @@ -201,14 +347,12 @@ Este release fue posible gracias a las contribuciones de: --- -## [Próximamente] - v2.1.0 +## [Próximamente] - v3.0.0 ### Planeado -- Soporte de operaciones con paréntesis en GUI (#44) -- Fix: Manejo de números decimales negativos (#49) -- Fix: Raíces pares de números negativos (#50) - Historial de operaciones - Más funciones matemáticas (raíz cuadrada, logaritmos, trigonometría) - Temas personalizables (claro/oscuro) - Exportar historial de cálculos +- Modo científico avanzado diff --git a/README.md b/README.md index 67548c5..f80c55a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) -Proyecto colaborativo para practicar flujo de trabajo en equipo usando **Python**. Calculadora con interfaz gráfica (GUI), interfaz de línea de comandos (CLI), testing automatizado y CI/CD. +Proyecto colaborativo para practicar flujo de trabajo en equipo usando **Python**.Calculadora con interfaz gráfica (GUI), interfaz de línea de comandos (CLI), testing automatizado y CI/CD. --- @@ -17,7 +17,7 @@ Proyecto colaborativo para practicar flujo de trabajo en equipo usando **Python* --- -## ✨ Características v2.0.0 +## ✨ Características v2.1.0 ### 🖥️ Interfaz Gráfica (GUI) @@ -26,15 +26,17 @@ Proyecto colaborativo para practicar flujo de trabajo en equipo usando **Python* - **Display de alta resolución** para números y resultados - **Soporte completo de teclado** + mouse - **Funciones científicas** integradas (abs, max, min) -- **Manejo visual de errores** +- **Operaciones con paréntesis** para expresiones complejas ✨ **NUEVO** +- **Manejo visual de errores** mejorado con validaciones específicas ### ⌨️ Atajos de Teclado | Tecla | Acción | | ------------------- | ----------------------- | | `0-9` | Ingresar dígitos | -| `. ` | Punto decimal | +| `.` | Punto decimal | | `+` `-` `*` `/` `^` | Operaciones matemáticas | +| `(` `)` | Paréntesis ✨ **NUEVO** | | `Enter` o `=` | Calcular resultado | | `Escape` | Limpiar display (Clear) | | `Backspace` | Borrar último carácter | @@ -49,6 +51,7 @@ Proyecto colaborativo para practicar flujo de trabajo en equipo usando **Python* - Tests unitarios con **pytest** - Tests de GUI con mocks de Tkinter +- **63+ tests unitarios** incluyendo 13 tests nuevos de paréntesis - Ejecutable sin interfaz gráfica (headless) - Ideal para CI/CD @@ -75,7 +78,7 @@ git clone https://github.com/WorkTeam01/team-practice.git cd team-practice # Instalar dependencias -pip install pytest +pip install -r requirements.txt ``` ### Ejecutar la Aplicación @@ -83,13 +86,13 @@ pip install pytest #### Interfaz Gráfica (GUI) ```bash -python gui.py +python src/gui.py ``` #### Interfaz de Línea de Comandos (CLI) ```bash -python main.py +python src/cli.py ``` ### Ejecutar Tests @@ -99,13 +102,16 @@ python main.py pytest -v # Tests de calculadora básica -pytest test_calculator.py -v +pytest tests/test_calculator.py -v # Tests de GUI -pytest test_gui_calculator.py -v +pytest tests/test_gui.py -v + +# Tests específicos de paréntesis +pytest tests/test_gui.py -k "parenthesis" -v # Tests con cobertura -pytest --cov=. -v +pytest --cov=src -v ``` --- @@ -114,22 +120,30 @@ pytest --cov=. -v ``` team-practice/ -├── calculator. py # Lógica de operaciones matemáticas -├── main.py # CLI - Interfaz de línea de comandos -├── gui.py # GUI - Interfaz gráfica con tkinter -├── test_calculator.py # Tests unitarios de calculator. py -├── test_gui_calculator.py # Tests de la interfaz gráfica -├── conftest.py # Fixtures de pytest (mocks de Tkinter) +├── src/ # Código fuente +│ ├── __init__.py +│ ├── calculator.py # Lógica de operaciones matemáticas +│ ├── cli.py # CLI - Interfaz de línea de comandos +│ └── gui.py # GUI - Interfaz gráfica con tkinter +├── tests/ # Tests +│ ├── __init__.py +│ ├── conftest.py # Fixtures de pytest (mocks de Tkinter) +│ ├── test_calculator.py # Tests unitarios de calculator.py +│ └── test_gui.py # Tests de la interfaz gráfica ├── .github/ │ ├── workflows/ -│ │ └── ci.yml # Pipeline de CI/CD -│ ├── ISSUE_TEMPLATE/ # Plantillas para issues +│ │ └── ci.yml # Pipeline de CI/CD +│ ├── ISSUE_TEMPLATE/ # Plantillas para issues │ ├── PULL_REQUEST_TEMPLATE/ # Plantillas para PRs │ ├── pull_request_template.md │ └── REVIEW_COMMENTS.md +├── docs/ # Documentación +│ ├── USER_GUIDE.md +│ └── screenshots/ ├── README.md # Este archivo ├── CHANGELOG.md # Historial de cambios ├── LICENSE # Licencia MIT +├── requirements.txt # Dependencias del proyecto └── .gitignore # Archivos ignorados por Git ``` @@ -145,6 +159,24 @@ team-practice/ - ➗ **División**: `a / b` - 🔢 **Potencia**: `a ^ b` +### Expresiones con Paréntesis ✨ **NUEVO v2.1.0** + +- 🔢 **Expresiones complejas**: `(2+3)*4`, `2*(3+4)`, `((2+3)*4)/5` +- 📊 **Paréntesis anidados**: Múltiples niveles de paréntesis soportados +- ⌨️ **Entrada flexible**: Desde teclado o botones +- ✅ **Validación automática**: Paréntesis balanceados y caracteres seguros + +**Ejemplos de uso:** + +``` +(2+3)*4 = 20 +2*(3+4) = 14 +(5-2)*(6+4) = 30 +((2+3)*4)/5 = 4 +(10/2)+5 = 10 +(2+3)^2 = 25 +``` + ### Funciones Científicas - `abs(x)` - Valor absoluto @@ -154,32 +186,35 @@ team-practice/ ### Manejo de Errores - ⚠️ División por cero detectada y manejada +- ⚠️ Paréntesis desbalanceados detectados ✨ **NUEVO** +- ⚠️ Raíces pares de números negativos (evita números complejos) ✨ **NUEVO** +- ⚠️ Decimales negativos con autocompletado mejorado ✨ **NUEVO** - 🛡️ Validación de entrada en ambas interfaces -- 📢 Mensajes de error claros +- 📢 Mensajes de error claros y específicos --- ## 🤝 Flujo de Trabajo Colaborativo -### 1. Antes de comenzar +### 1.Antes de comenzar ```bash -# Actualizar rama main -git checkout main -git pull origin main +# Actualizar rama dev +git checkout dev +git pull origin dev # Crear rama para tu feature git checkout -b feature/nombre-descriptivo ``` -### 2. Durante el desarrollo +### 2.Durante el desarrollo - ✅ Commits frecuentes y descriptivos - ✅ Seguir [Conventional Commits](https://www.conventionalcommits.org/) - ✅ Escribir tests para nuevas funcionalidades - ✅ Ejecutar tests localmente antes de push -### 3. Al finalizar +### 3.Al finalizar ```bash # Push de tu rama @@ -202,11 +237,11 @@ tipo: descripción breve Descripción más detallada si es necesario Ejemplos: -feat: agregar soporte de teclado para calculadora -fix: corregir división por cero -docs: actualizar instrucciones de instalación -test: agregar tests para botones numéricos -refactor: eliminar lógica redundante en operadores +feat: agregar soporte de paréntesis en calculadora +fix: corregir validación de decimales negativos +docs: actualizar README con nuevas características +test: agregar tests para paréntesis anidados +refactor: reorganizar estructura del proyecto ``` **Tipos de commit:** @@ -234,45 +269,49 @@ refactor: eliminar lógica redundante en operadores ### Proceso de Contribución -1. **Asigna o crea un issue** usando las plantillas proporcionadas - - Para bugs: usa la plantilla de "reporte de error" - - Para nuevas funciones: usa la plantilla de "nueva funcionalidad" -2. **Crea tu rama** desde `dev` (no desde `main`) +1.**Asigna o crea un issue** usando las plantillas proporcionadas - ```bash - git checkout dev - git pull origin dev - git checkout -b feature/mi-funcionalidad - ``` +- Para bugs: usa la plantilla de "reporte de error" +- Para nuevas funciones: usa la plantilla de "nueva funcionalidad" -3. **Implementa tu cambio** + 2.**Crea tu rama** desde `dev` (no desde `main`) + +```bash +git checkout dev +git pull origin dev +git checkout -b feature/mi-funcionalidad +``` + +3.**Implementa tu cambio** - Escribe código limpio y documentado - Sigue las convenciones del proyecto -4. **Agrega tests** si aplica + 4.**Agrega tests** si aplica - ```bash - # Ejecutar tests localmente - pytest -v - ``` + ```bash + # Ejecutar tests localmente + pytest -v + ``` -5. **Actualiza documentación** si es necesario + 5.**Actualiza documentación** si es necesario - - README.md - - Docstrings en el código - - CHANGELOG.md (si es un cambio significativo) + - README.md + - Docstrings en el código + - CHANGELOG.md (si es un cambio significativo) -6. **Crea Pull Request** usando la plantilla de PR + 6.**Crea Pull Request** usando la plantilla de PR - - Describe claramente los cambios - - Referencia el issue relacionado - - Agrega capturas de pantalla si hay cambios visuales + - Describe claramente los cambios + - Referencia el issue relacionado + - Agrega capturas de pantalla si hay cambios visuales -7. **Espera code review** - - Responde a los comentarios - - Realiza los cambios solicitados -8. **Mergea** después de aprobación del equipo + 7.**Espera code review** + + - Responde a los comentarios + - Realiza los cambios solicitados + + 8.**Mergea** después de aprobación del equipo --- @@ -280,7 +319,7 @@ refactor: eliminar lógica redundante en operadores El proyecto sigue **[Versionamiento Semántico](https://semver.org/)**: -### Versión Actual: **v2.0.0** 🎉 +### Versión Actual: **v2.1.0** 🎉 **Changelog completo:** @@ -288,6 +327,7 @@ El proyecto sigue **[Versionamiento Semántico](https://semver.org/)**: **Versiones disponibles:** +- **v2.1.0** (2025-12-03) - Paréntesis + Reorganización + Bug fixes - **v2.0.0** (2025-11-28) - Interfaz gráfica + Testing + CI/CD - **v1.0.0** (2025-11-04) - Calculadora CLI básica @@ -302,11 +342,14 @@ El proyecto sigue **[Versionamiento Semántico](https://semver.org/)**: pytest -v # Tests específicos -pytest test_calculator.py -v -pytest test_gui_calculator.py -v +pytest tests/test_calculator.py -v +pytest tests/test_gui.py -v + +# Tests de paréntesis +pytest tests/test_gui.py -k "parenthesis" -v # Con cobertura -pytest --cov=. --cov-report=html -v +pytest --cov=src --cov-report=html -v # Tests en modo verbose con detalles pytest -vv @@ -314,9 +357,9 @@ pytest -vv ### Estructura de Tests -- **`test_calculator.py`**: Tests de lógica matemática -- **`test_gui_calculator.py`**: Tests de interfaz gráfica -- **`conftest.py`**: Fixtures y mocks de Tkinter +- **`tests/test_calculator.py`**: Tests de lógica matemática +- **`tests/test_gui.py`**: Tests de interfaz gráfica (63+ tests) +- **`tests/conftest.py`**: Fixtures y mocks de Tkinter --- @@ -382,15 +425,14 @@ git branch -d feature/mi-rama --- -## 🚧 Próximas Características (v2.1.0) +## 🚧 Próximas Características (v3.0.0) -- [ ] Soporte de operaciones con paréntesis (#44) -- [ ] Fix: Manejo de números decimales negativos (#49) -- [ ] Fix: Raíces pares de números negativos (#50) - [ ] Historial de operaciones - [ ] Más funciones matemáticas (√, log, sin, cos, tan) - [ ] Temas personalizables (claro/oscuro) - [ ] Exportar historial de cálculos +- [ ] Modo científico avanzado +- [ ] Gráficos de funciones --- @@ -398,15 +440,15 @@ git branch -d feature/mi-rama Este proyecto fue desarrollado colaborativamente por: -- **[@Jandres25](https://github.com/Jandres25)** - Coordinador, GUI, CI/CD, Testing -- **[@Jhos3ph](https://github.com/Jhos3ph)** - Funciones científicas, Lógica, Refactoring -- **[@alexricardotapiacarita-ai](https://github.com/alexricardotapiacarita-ai)** - Diseño GUI, Documentación +- **[@Jandres25](https://github.com/Jandres25)** - Coordinador, GUI, CI/CD, Testing, Paréntesis +- **[@Jhos3ph](https://github.com/Jhos3ph)** - Funciones científicas, Lógica, Refactoring, Bug fixes +- **[@alexricardotapiacarita-ai](https://github.com/alexricardotapiacarita-ai)** - Diseño GUI, Documentación, UX --- ## 📄 Licencia -Este proyecto está bajo la Licencia MIT. Ver [LICENSE](LICENSE) para más detalles. +Este proyecto está bajo la Licencia MIT.Ver [LICENSE](LICENSE) para más detalles. --- @@ -416,12 +458,14 @@ Este proyecto está bajo la Licencia MIT. Ver [LICENSE](LICENSE) para más detal - **Framework GUI**: Tkinter - **Framework Testing**: Pytest - **CI/CD**: GitHub Actions -- **Commits**: 60+ -- **Pull Requests**: 24+ -- **Issues Cerradas**: 15+ +- **Commits**: 75+ +- **Pull Requests**: 35+ +- **Issues Cerradas**: 22+ +- **Tests**: 63+ tests unitarios +- **Cobertura**: Alta cobertura de código --- -**¡Happy coding y colaboración efectiva! ** 🐍✨🚀 +**¡Happy coding y colaboración efectiva!** 🐍✨🚀 Para más información, consulta el [CHANGELOG.md](CHANGELOG.md) para ver el historial completo de cambios. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..536e546 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +# Dependencias para desarrollo y testing +pytest>=7.0.0 diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..2a61ebb --- /dev/null +++ b/src/__init__.py @@ -0,0 +1,7 @@ +"""Paquete calculadora con GUI y CLI. + +Módulos: + calculator: Funciones matemáticas básicas + gui: Interfaz gráfica con tkinter + cli: Interfaz de línea de comandos +""" \ No newline at end of file diff --git a/calculator.py b/src/calculator.py similarity index 93% rename from calculator.py rename to src/calculator.py index 4995fb6..0a379ea 100644 --- a/calculator.py +++ b/src/calculator.py @@ -105,6 +105,13 @@ def power(base: float, exponent: float) -> float: >>> power(5, 0) 1 """ + + if base < 0 and exponent != 0: + inv_exp = 1 / exponent + if abs(inv_exp - round(inv_exp)) < 1e-10: + if round(inv_exp) % 2 == 0: + raise ValueError("Raíz negativa") + return base ** exponent def valor_maximo(a: float, b: float) -> float: diff --git a/main.py b/src/cli.py similarity index 93% rename from main.py rename to src/cli.py index 4154386..eafa893 100644 --- a/main.py +++ b/src/cli.py @@ -1,11 +1,13 @@ -#!/usr/bin/env python3 """Archivo principal de ejemplo para el proyecto Team Practice. Demuestra cómo usar los módulos del proyecto y cómo estructurar un punto de entrada principal. """ -from calculator import add, subtract, multiply, divide, power, valor_maximo, valor_minimo, abs_value +try: + from .calculator import add, subtract, multiply, divide, power, valor_maximo, valor_minimo, abs_value +except ImportError: + from calculator import add, subtract, multiply, divide, power, valor_maximo, valor_minimo, abs_value def main(): """Función principal del programa.""" diff --git a/gui.py b/src/gui.py similarity index 55% rename from gui.py rename to src/gui.py index 6ada399..308502d 100644 --- a/gui.py +++ b/src/gui.py @@ -1,5 +1,19 @@ +"""Interfaz gráfica de usuario (GUI) para la calculadora. + +Este módulo implementa una calculadora con interfaz tkinter que incluye: +- Operaciones básicas: suma, resta, multiplicación, división, potencia +- Funciones científicas: valor absoluto, máximo, mínimo +- Soporte para paréntesis en expresiones matemáticas +- Soporte para teclado y números negativos +- Manejo de errores con mensajes visuales +""" import tkinter as tk -from calculator import (add, subtract, multiply, divide, power, valor_maximo, valor_minimo, abs_value) +import re + +try: + from .calculator import add, subtract, multiply, divide, power, valor_maximo, valor_minimo, abs_value +except ImportError: + from calculator import add, subtract, multiply, divide, power, valor_maximo, valor_minimo, abs_value class CalculatorGUI: def __init__(self, root): @@ -8,15 +22,17 @@ def __init__(self, root): self.root.geometry("330x450") self.root.configure(bg="#1E1E1E") - # Estado - self.current_value = "" - self.operator = None - self.first_number = None + # Variables de estado de la calculadora + self.expression = "" # Guarda expresiones completas como "(2+3)*4" + self.current_value = "" + self.operator = None + self.first_number = None + self.use_expression_mode = False # True cuando usamos paréntesis - # Interfaz de usuario + # Crear la interfaz self.create_widgets() - # Activar captura de teclado + # Habilitar el uso del teclado self.root.bind('', self.handle_keypress) def create_widgets(self): @@ -102,6 +118,10 @@ def create_widgets(self): cmd = self.clear_click elif txt == '⌫': cmd = self.backspace_click + elif txt == '(': + cmd = self.open_parenthesis_click + elif txt == ')': + cmd = self.close_parenthesis_click elif txt in ['abs', 'max', 'min']: cmd = lambda t=txt: self.scientific_click(t) else: @@ -129,9 +149,10 @@ def create_widgets(self): def handle_keypress(self, event): - """Maneja las teclas presionadas por el usuario. + """Maneja las teclas presionadas por el usuario. - Mapea las teclas del teclado a las funciones de la calculadora. + Mapea las teclas del teclado a las funciones de la calculadora, + incluyendo paréntesis. """ key = event.char @@ -151,6 +172,13 @@ def handle_keypress(self, event): elif key == '.': self.decimal_click() + # Paréntesis desde el teclado + elif key == '(': + self.open_parenthesis_click() + + elif key == ')': + self.close_parenthesis_click() + # Calcular (Enter o =) elif key in ['\r', '\n', '=']: self.equals_click() @@ -164,55 +192,129 @@ def handle_keypress(self, event): self.backspace_click() + def open_parenthesis_click(self): + """Agrega un paréntesis de apertura a la expresión. + + Activa el modo de expresión para evaluar con paréntesis. + + Examples: + >>> # Usuario presiona "(" + >>> # Display muestra: "(" + >>> # use_expression_mode = True + """ + # Si hay algo en current_value o ya hay operación en curso, transferir a expression + if not self.use_expression_mode: + if self.first_number is not None: + self.expression = str(self.first_number) + if self.operator: + self.expression += self.operator + if self.current_value: + self.expression += self.current_value + elif self.current_value: + self.expression = self.current_value + + self.use_expression_mode = True + self.expression += '(' + self.display.delete(0, tk.END) + self.display.insert(0, self.expression) + + + def close_parenthesis_click(self): + """Agrega un paréntesis de cierre a la expresión. + + Examples: + >>> # Expression: "(2+3" + >>> # Usuario presiona ")" + >>> # Display muestra: "(2+3)" + """ + self.use_expression_mode = True + self.expression += ')' + self.display.delete(0, tk.END) + self.display.insert(0, self.expression) + + def number_button_click(self, valor): """Maneja clicks de botones numéricos. + Funciona tanto en modo normal como en modo de expresión con paréntesis. + Args: - value (str): Dígito presionado (0-9) + valor (str): Dígito presionado (0-9) Examples: >>> # Usuario presiona 2, 3, 5 >>> # Display muestra: "235" + >>> # Con paréntesis: "(2+3)*5" """ - # Validar que sea un dígito + # Asegurarnos de que es un número if not str(valor).isdigit(): self.show_error("Número inválido") return - self.current_value += str(valor) - self.display.delete(0, tk.END) - self.display.insert(0, self.current_value) + if self.use_expression_mode: + self.expression += str(valor) + self.display.delete(0, tk.END) + self.display.insert(0, self.expression) + else: + self.current_value += str(valor) + self.display.delete(0, tk.END) + self.display.insert(0, self.current_value) def decimal_click(self): - """Maneja click del botón decimal. + """Maneja click del botón decimal con validaciones mejoradas. + + Funciona en modo normal y en modo de expresión con paréntesis. - Agrega un punto decimal solo si no existe uno ya en el número actual. - - Examples: - >>> "5" → click(.) → "5." - >>> "5." → click(.) → "5." (no cambia) - >>> "" → click(.) → "0." - """ - # Validar que current_value sea válido antes de agregar punto - if self.current_value and self.current_value != '-': - try: - float(self.current_value) - except ValueError: - self.show_error("Número inválido") - return + Corrige casos como: + "-" + "." → "-0." + "-.3" → "-0.3" - if '.' not in self.current_value: - if not self.current_value: - self.current_value = "0" - self.current_value += '.' + Previene: + "-." como número inválido. + """ + if self.use_expression_mode: + # Si estamos en modo expresión, solo agregamos el punto + self.expression += '.' + self.display.delete(0, tk.END) + self.display.insert(0, self.expression) + return + + # Si escribieron solo "-" y presionan ".", convertir a "-0." + if self.current_value == "-": + self.current_value = "-0." + self.display.delete(0, tk.END) + self.display.insert(0, self.current_value) + return + + # Si no hay nada, empezar con "0." + if not self.current_value: + self.current_value = "0." self.display.delete(0, tk.END) self.display.insert(0, self.current_value) + return + + # No permitir más de un punto decimal + if '.' in self.current_value: + return + + # Verificar que lo que hay sea un número válido + try: + float(self.current_value) + except ValueError: + self.show_error("Número inválido") + return + + # Todo bien, agregar el punto + self.current_value += '.' + self.display.delete(0, tk.END) + self.display.insert(0, self.current_value) def operation_click(self, operation): """Maneja clicks de operadores matemáticos. + Funciona en modo normal y en modo de expresión con paréntesis. Guarda el primer número y operador para calcular cuando el usuario presione "=". @@ -221,12 +323,22 @@ def operation_click(self, operation): Examples: >>> # Usuario: 5 + 3 = - >>> # 1. Ingresa "5" - >>> # 2. Click "+": first_number=5, operator="+" + >>> # 1.Ingresa "5" + >>> # 2.Click "+": first_number=5, operator="+" >>> # 3. Ingresa "3" - >>> # 4. Click "=": calcula 5+3=8 + >>> # 4.Click "=": calcula 5+3=8 + >>> + >>> # Con paréntesis: (2+3)*4 + >>> # expression = "(2+3)*4" """ - # Permitir números negativos si se presiona '-' al inicio + # Si estamos en modo expresión (con paréntesis) + if self.use_expression_mode: + self.expression += operation + self.display.delete(0, tk.END) + self.display.insert(0, self.expression) + return + + # Caso especial: permitir escribir números negativos if operation == '-' and (self.current_value == "" or self.current_value is None): self.current_value = '-' self.display.delete(0, tk.END) @@ -235,27 +347,27 @@ def operation_click(self, operation): if operation == '-' and self.current_value == '-': return - # Cambiar de operador si ya hay uno seleccionado + # Si cambiaron de opinión sobre el operador, actualizarlo if not self.current_value: if self.first_number is not None: self.operator = operation return - # Validar que sea un número válido + # Verificar que lo ingresado sea un número válido try: value = float(self.current_value) except ValueError: self.show_error("Número inválido") return - # Guardar primer número + # Primera vez: guardar el número y el operador if self.first_number is None: self.first_number = value self.operator = operation self.current_value = "" return - # Calcular operación pendiente antes de la nueva + # Ya hay una operación pendiente, calcularla primero if self.first_number is not None and self.operator is not None: self.equals_click() try: @@ -267,16 +379,60 @@ def operation_click(self, operation): self.current_value = "" def equals_click(self): - """Calcula el resultado de la operación actual. + """Calcula el resultado de la operación o expresión actual. - Usa las funciones de calculator. py para realizar el cálculo. + Soporta dos modos: + 1.Modo normal: operaciones simples con dos números + 2.Modo expresión: expresiones completas con paréntesis Examples: - >>> # Usuario: 5 + 3 = + >>> # Modo normal: 5 + 3 = >>> # first_number=5, operator="+", current_value="3" >>> # Ejecuta: add(5, 3) = 8 >>> # Display: "8" + >>> + >>> # Modo expresión: (2+3)*4 = + >>> # expression = "(2+3)*4" + >>> # Evalúa: eval("(2+3)*4") = 20 + >>> # Display: "20" """ + # Si estamos en modo expresión (con paréntesis) + if self.use_expression_mode and self.expression: + try: + # Verificar que los paréntesis estén balanceados + if self.expression.count('(') != self.expression.count(')'): + self.show_error('Paréntesis desbalanceados') + return + + # Por seguridad, solo permitir números y operadores básicos + if not re.match(r'^[\d\s\+\-\*\/\^\(\)\.]+$', self.expression): + self.show_error('Expresión inválida') + return + + # Python usa ** para potencias, no ^ + expr = self.expression.replace('^', '**') + + # Calcular el resultado + result = eval(expr) + + # Mostrar en pantalla + self.display.delete(0, tk.END) + self.display.insert(0, str(result)) + + # Preparar para la siguiente operación + self.current_value = str(result) + self.expression = str(result) + self.use_expression_mode = False + + except ZeroDivisionError: + self.show_error("No se puede dividir por 0") + except SyntaxError: + self.show_error("Sintaxis incorrecta") + except Exception as e: + self.show_error("Error en la expresión") + return + + # Modo normal (operaciones simples sin paréntesis) if self.first_number is not None and self.operator is not None and not self.current_value: self.show_error("Ingresa el segundo número") return @@ -307,9 +463,13 @@ def equals_click(self): self.first_number = None self.operator = None - except ValueError: - self.show_error("Entrada inválida") + except ValueError as e: + if str(e) == "Raíz negativa": + self.show_error("Raíz negativa") + else: + self.show_error("Entrada inválida") return + except ZeroDivisionError: self.show_error("No se puede dividir por 0") return @@ -319,46 +479,61 @@ def equals_click(self): def clear_click(self): - """Limpia completamente el display y resetea el estado de la calculadora. + """Limpia completamente el display y resetea el estado de la calculadora. + + Resetea tanto el modo normal como el modo de expresión. Resetea: - current_value: cadena vacía - operator: None - first_number: None + - expression: cadena vacía + - use_expression_mode: False - Display: vacío Examples: - >>> # Display muestra: "235" + >>> # Display muestra: "(2+3)*4" >>> # Usuario presiona C >>> # Display muestra: "" """ self.current_value = "" self.operator = None self.first_number = None + self.expression = "" + self.use_expression_mode = False self.display.delete(0, tk.END) - self.error_label.config(text="") # Limpiar también el error + self.error_label.config(text="") # Quitar cualquier mensaje de error def backspace_click(self): """Elimina el último carácter del display. + Funciona en modo normal y en modo de expresión. Si el display está vacío, no hace nada. Examples: >>> # Display muestra: "1234" >>> # Usuario presiona ⌫ >>> # Display muestra: "123" - >>> # Usuario presiona ⌫ tres veces más - >>> # Display muestra: "" + >>> # Display muestra: "(2+3)" + >>> # Usuario presiona ⌫ + >>> # Display muestra: "(2+3" """ - if self.current_value: + if self.use_expression_mode and self.expression: + self.expression = self.expression[:-1] + # Si ya no quedan paréntesis, volver al modo simple + if '(' not in self.expression and ')' not in self.expression: + self.use_expression_mode = False + self.display.delete(0, tk.END) + self.display.insert(0, self.expression) + elif self.current_value: self.current_value = self.current_value[:-1] self.display.delete(0, tk.END) self.display.insert(0, self.current_value) def show_error(self, message): - """Muestra un mensaje de error en el label de errores. + """Muestra un mensaje de error en el label de errores. Args: message (str): Mensaje de error a mostrar @@ -366,22 +541,36 @@ def show_error(self, message): Examples: >>> self.show_error("División por 0") >>> # Label de error: "⚠️ División por 0" + >>> self.show_error("Paréntesis desbalanceados") + >>> # Label de error: "⚠️ Paréntesis desbalanceados" """ self.error_label.config(text=f"⚠️ {message}") - # Limpiar error después de 3 segundos + # El mensaje desaparece solo después de 3 segundos self.root.after(3000, lambda: self.error_label.config(text="")) - # Resetear estado + # Limpiar todo para empezar de nuevo self.current_value = "" self.first_number = None self.operator = None + self.expression = "" + self.use_expression_mode = False # Limpiar el display también self.display.delete(0, tk.END) def unary_operation(self, func): + """Ejecuta operaciones unarias (que solo necesitan un número). + + Args: + func (str): Nombre de la función a ejecutar (abs, cos, sin, tan) + + Examples: + >>> # Display: "-5" + >>> # Usuario presiona "abs" + >>> # Display: "5" + """ if self.current_value: if self.current_value == "-": self.show_error("Número incompleto") diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..96a5d7b --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,4 @@ +"""Suite de tests para Team Practice Calculator. + +Tests unitarios para los módulos calculator y gui. +""" \ No newline at end of file diff --git a/conftest.py b/tests/conftest.py similarity index 90% rename from conftest.py rename to tests/conftest.py index 2800878..e0ddbc8 100644 --- a/conftest.py +++ b/tests/conftest.py @@ -1,3 +1,8 @@ +"""Configuración de pytest y fixtures para tests. + +Este módulo contiene fixtures y configuración compartida para todos los tests. +Incluye mocks de componentes tkinter para ejecutar tests sin interfaz gráfica. +""" import pytest import tkinter as tk diff --git a/test_calculator.py b/tests/test_calculator.py similarity index 90% rename from test_calculator.py rename to tests/test_calculator.py index 3d812e9..636d34b 100644 --- a/test_calculator.py +++ b/tests/test_calculator.py @@ -6,7 +6,7 @@ import pytest -from calculator import add, subtract, multiply, divide, power, valor_maximo, valor_minimo, abs_value +from src.calculator import add, subtract, multiply, divide, power, valor_maximo, valor_minimo, abs_value def test_add(): @@ -53,7 +53,8 @@ def test_power(): assert power(-2, 3) == -8 assert power(-2, 0) == 1 assert power(-2, -3) == -0.125 - # assert power(-4, 0.5) == -2.0, "Error: la raíz cuadrada de un número negativo no es real" + with pytest.raises(ValueError, match="Raíz negativa"): + power(-4, 0.5) def test_valor_maximo(): diff --git a/test_gui_calculator.py b/tests/test_gui.py similarity index 57% rename from test_gui_calculator.py rename to tests/test_gui.py index c2a78e6..794d43b 100644 --- a/test_gui_calculator.py +++ b/tests/test_gui.py @@ -4,7 +4,7 @@ Para correr los tests: pytest test_gui_calculator.py -v """ import tkinter as tk -from gui import CalculatorGUI +from src.gui import CalculatorGUI # ============================================================================ @@ -512,6 +512,511 @@ def test_valor_minimo(): root.destroy() +# ============================================================================ +# TESTS DE PARÉNTESISS +# ============================================================================ + +def test_parenthesis_basic(): + """Test de operaciones básicas con paréntesis.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: (2+3)*4 = 20 + calc.open_parenthesis_click() + calc.number_button_click("2") + calc.operation_click("+") + calc.number_button_click("3") + calc.close_parenthesis_click() + calc.operation_click("*") + calc.number_button_click("4") + calc.equals_click() + assert float(calc.current_value) == 20.0 + + calc.clear_click() + + # Caso 2: 2*(3+4) = 14 + calc.number_button_click("2") + calc.operation_click("*") + calc.open_parenthesis_click() + calc.number_button_click("3") + calc.operation_click("+") + calc.number_button_click("4") + calc.close_parenthesis_click() + calc.equals_click() + assert float(calc.current_value) == 14.0 + + calc.clear_click() + + # Caso 3: (5-2)*(6+4) = 30 + calc.open_parenthesis_click() + calc.number_button_click("5") + calc.operation_click("-") + calc.number_button_click("2") + calc.close_parenthesis_click() + calc.operation_click("*") + calc.open_parenthesis_click() + calc.number_button_click("6") + calc.operation_click("+") + calc.number_button_click("4") + calc.close_parenthesis_click() + calc.equals_click() + assert float(calc.current_value) == 30.0 + + calc.clear_click() + + # Caso 4: (10/2)+5 = 10 + calc.open_parenthesis_click() + calc.number_button_click("1") + calc.number_button_click("0") + calc.operation_click("/") + calc.number_button_click("2") + calc.close_parenthesis_click() + calc.operation_click("+") + calc.number_button_click("5") + calc.equals_click() + assert float(calc.current_value) == 10.0 + + root.destroy() + + +def test_parenthesis_nested(): + """Test de paréntesis anidados.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: ((2+3)*4)/5 = 4 + calc.open_parenthesis_click() + calc.open_parenthesis_click() + calc.number_button_click("2") + calc.operation_click("+") + calc.number_button_click("3") + calc.close_parenthesis_click() + calc.operation_click("*") + calc.number_button_click("4") + calc.close_parenthesis_click() + calc.operation_click("/") + calc.number_button_click("5") + calc.equals_click() + assert float(calc.current_value) == 4.0 + + calc.clear_click() + + # Caso 2: (2+(3*4)) = 14 + calc.open_parenthesis_click() + calc.number_button_click("2") + calc.operation_click("+") + calc.open_parenthesis_click() + calc.number_button_click("3") + calc.operation_click("*") + calc.number_button_click("4") + calc.close_parenthesis_click() + calc.close_parenthesis_click() + calc.equals_click() + assert float(calc.current_value) == 14.0 + + calc.clear_click() + + # Caso 3: ((10-5)*2)+3 = 13 + calc.open_parenthesis_click() + calc.open_parenthesis_click() + calc.number_button_click("1") + calc.number_button_click("0") + calc.operation_click("-") + calc.number_button_click("5") + calc.close_parenthesis_click() + calc.operation_click("*") + calc.number_button_click("2") + calc.close_parenthesis_click() + calc.operation_click("+") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == 13.0 + + root.destroy() + + +def test_parenthesis_with_decimals(): + """Test de paréntesis con números decimales.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: (2.5+3.5)*2 = 12 + calc.open_parenthesis_click() + calc.number_button_click("2") + calc.decimal_click() + calc.number_button_click("5") + calc.operation_click("+") + calc.number_button_click("3") + calc.decimal_click() + calc.number_button_click("5") + calc.close_parenthesis_click() + calc.operation_click("*") + calc.number_button_click("2") + calc.equals_click() + assert float(calc.current_value) == 12.0 + + calc.clear_click() + + # Caso 2: (10.5/2)+1.5 = 6.75 + calc.open_parenthesis_click() + calc.number_button_click("1") + calc.number_button_click("0") + calc.decimal_click() + calc.number_button_click("5") + calc.operation_click("/") + calc.number_button_click("2") + calc.close_parenthesis_click() + calc.operation_click("+") + calc.number_button_click("1") + calc.decimal_click() + calc.number_button_click("5") + calc.equals_click() + assert float(calc.current_value) == 6.75 + + root.destroy() + + +def test_parenthesis_with_negatives(): + """Test de paréntesis con números negativos.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: (-5+3)*2 = -4 + calc.open_parenthesis_click() + calc.operation_click("-") + calc.number_button_click("5") + calc.operation_click("+") + calc.number_button_click("3") + calc.close_parenthesis_click() + calc.operation_click("*") + calc.number_button_click("2") + calc.equals_click() + assert float(calc.current_value) == -4.0 + + calc.clear_click() + + # Caso 2: (10-15)*2 = -10 + calc.open_parenthesis_click() + calc.number_button_click("1") + calc.number_button_click("0") + calc.operation_click("-") + calc.number_button_click("1") + calc.number_button_click("5") + calc.close_parenthesis_click() + calc.operation_click("*") + calc.number_button_click("2") + calc.equals_click() + assert float(calc.current_value) == -10.0 + + root.destroy() + + +def test_parenthesis_with_power(): + """Test de paréntesis con potencias.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: (2+3)^2 = 25 + calc.open_parenthesis_click() + calc.number_button_click("2") + calc.operation_click("+") + calc.number_button_click("3") + calc.close_parenthesis_click() + calc.operation_click("^") + calc.number_button_click("2") + calc.equals_click() + assert float(calc.current_value) == 25.0 + + calc.clear_click() + + # Caso 2: 2^(3+1) = 16 + calc.number_button_click("2") + calc.operation_click("^") + calc.open_parenthesis_click() + calc.number_button_click("3") + calc.operation_click("+") + calc.number_button_click("1") + calc.close_parenthesis_click() + calc.equals_click() + assert float(calc.current_value) == 16.0 + + root.destroy() + + +def test_parenthesis_unbalanced(): + """Test de validación de paréntesis desbalanceados.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: Falta paréntesis de cierre: (2+3 + calc.open_parenthesis_click() + calc.number_button_click("2") + calc.operation_click("+") + calc.number_button_click("3") + calc.equals_click() + + # Debe mostrar error y limpiar + assert calc.current_value == "" + assert calc.expression == "" + assert calc.use_expression_mode == False + + calc.clear_click() + + # Caso 2: Falta paréntesis de apertura: 2+3) + calc.number_button_click("2") + calc.operation_click("+") + calc.number_button_click("3") + calc.close_parenthesis_click() + calc.equals_click() + + # Debe mostrar error y limpiar + assert calc.current_value == "" + assert calc.expression == "" + + root.destroy() + + +def test_parenthesis_expression_mode(): + """Test de activación del modo de expresión.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Inicialmente no está en modo expresión + assert calc.use_expression_mode == False + assert calc.expression == "" + + # Al presionar paréntesis de apertura, se activa + calc.open_parenthesis_click() + assert calc.use_expression_mode == True + assert calc.expression == "(" + + # Continuar agregando a la expresión + calc.number_button_click("5") + assert calc.expression == "(5" + + calc.operation_click("+") + assert calc.expression == "(5+" + + calc.number_button_click("3") + assert calc.expression == "(5+3" + + calc.close_parenthesis_click() + assert calc.expression == "(5+3)" + + # Calcular + calc.equals_click() + assert float(calc.current_value) == 8.0 + + # Después de calcular, se desactiva el modo expresión + assert calc.use_expression_mode == False + + root.destroy() + + +def test_parenthesis_clear(): + """Test de limpiar expresiones con paréntesis.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Construir expresión + calc.open_parenthesis_click() + calc.number_button_click("2") + calc.operation_click("+") + calc.number_button_click("3") + calc.close_parenthesis_click() + + assert calc.use_expression_mode == True + assert calc.expression == "(2+3)" + + # Limpiar + calc.clear_click() + + # Todo debe estar limpio + assert calc.current_value == "" + assert calc.expression == "" + assert calc.use_expression_mode == False + assert calc.operator is None + assert calc.first_number is None + + root.destroy() + + +def test_parenthesis_backspace(): + """Test de borrar caracteres en expresiones con paréntesis.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Construir expresión: (2+3) + calc.open_parenthesis_click() + calc.number_button_click("2") + calc.operation_click("+") + calc.number_button_click("3") + calc.close_parenthesis_click() + + assert calc.expression == "(2+3)" + + # Borrar caracteres + calc.backspace_click() + assert calc.expression == "(2+3" + + calc.backspace_click() + assert calc.expression == "(2+" + + calc.backspace_click() + assert calc.expression == "(2" + + calc.backspace_click() + assert calc.expression == "(" + + calc.backspace_click() + assert calc.expression == "" + + # Al borrar todos los paréntesis, debe salir del modo expresión + assert calc.use_expression_mode == False + + root.destroy() + + +def test_parenthesis_display_update(): + """Test de que el display se actualiza correctamente con paréntesis.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Construir expresión y verificar que el display se actualiza + calc.open_parenthesis_click() + display_value = calc.display.get() + assert display_value == "(" + + calc.number_button_click("5") + display_value = calc.display.get() + assert display_value == "(5" + + calc.operation_click("+") + display_value = calc.display.get() + assert display_value == "(5+" + + calc.number_button_click("3") + display_value = calc.display.get() + assert display_value == "(5+3" + + calc.close_parenthesis_click() + display_value = calc.display.get() + assert display_value == "(5+3)" + + calc.equals_click() + display_value = calc.display.get() + assert display_value == "8.0" or display_value == "8" + + root.destroy() + + +def test_parenthesis_mixed_mode(): + """Test de compatibilidad entre modo normal y modo expresión.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Operación normal: 5 + 3 = 8 + calc.number_button_click("5") + calc.operation_click("+") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == 8.0 + + calc.clear_click() + + # Ahora con paréntesis: (5+3)*2 = 16 + calc.open_parenthesis_click() + calc.number_button_click("5") + calc.operation_click("+") + calc.number_button_click("3") + calc.close_parenthesis_click() + calc.operation_click("*") + calc.number_button_click("2") + calc.equals_click() + assert float(calc.current_value) == 16.0 + + calc.clear_click() + + # De nuevo modo normal: 10 - 2 = 8 + calc.number_button_click("1") + calc.number_button_click("0") + calc.operation_click("-") + calc.number_button_click("2") + calc.equals_click() + assert float(calc.current_value) == 8.0 + + root.destroy() + + +def test_parenthesis_complex_expression(): + """Test de expresiones complejas con múltiples operadores.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: ((2+3)*(4-1))/5 = 3 + calc.open_parenthesis_click() + calc.open_parenthesis_click() + calc.number_button_click("2") + calc.operation_click("+") + calc.number_button_click("3") + calc.close_parenthesis_click() + calc.operation_click("*") + calc.open_parenthesis_click() + calc.number_button_click("4") + calc.operation_click("-") + calc.number_button_click("1") + calc.close_parenthesis_click() + calc.close_parenthesis_click() + calc.operation_click("/") + calc.number_button_click("5") + calc.equals_click() + assert float(calc.current_value) == 3.0 + + calc.clear_click() + + # Caso 2: (10/(2+3))*4 = 8 + calc.open_parenthesis_click() + calc.number_button_click("1") + calc.number_button_click("0") + calc.operation_click("/") + calc.open_parenthesis_click() + calc.number_button_click("2") + calc.operation_click("+") + calc.number_button_click("3") + calc.close_parenthesis_click() + calc.close_parenthesis_click() + calc.operation_click("*") + calc.number_button_click("4") + calc.equals_click() + assert float(calc.current_value) == 8.0 + + root.destroy() + + +def test_parenthesis_division_by_zero(): + """Test de división por cero dentro de paréntesis.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # (10/0)+5 debe dar error + calc.open_parenthesis_click() + calc.number_button_click("1") + calc.number_button_click("0") + calc.operation_click("/") + calc.number_button_click("0") + calc.close_parenthesis_click() + calc.operation_click("+") + calc.number_button_click("5") + calc.equals_click() + + # Debe mostrar error y limpiar + assert calc.current_value == "" + assert calc.expression == "" + assert calc.use_expression_mode == False + + root.destroy() + + # ============================================================================ # TESTS DE CASOS EXTREMOS Y ERRORES # ============================================================================