diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f4237c6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + branches: [main, dev] + pull_request: + branches: [main, dev] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.12"] + + steps: + - uses: actions/checkout@v3 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest + + - name: Run tests + run: pytest -v diff --git a/CHANGELOG.md b/CHANGELOG.md index 6916e85..6afb671 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,177 @@ Todos los cambios notables en este proyecto serán documentados en este archivo. El formato está basado en [Keep a Changelog](https://keepachangelog.com/es-ES/1.0.0/), y este proyecto se adhiere a [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +--- + +## [2.0.0] - 2025-11-28 + +### 🎉 Major Release - Interfaz Gráfica + Testing Automatizado + +Este release marca una evolución significativa del proyecto, introduciendo una interfaz gráfica completa, testing automatizado robusto, soporte de teclado y múltiples mejoras de usabilidad. + +### Agregado + +#### Interfaz Gráfica de Usuario (GUI) + +- **Calculadora visual** con tkinter (#15, #16, #17) + - Ventana principal con diseño moderno + - Display interactivo de alta resolución para números y resultados + - Grid de botones organizados por función + - Tema oscuro profesional con paleta de colores elegante (#18, #19, #20, #21) + - Diseño responsive con grid layout adaptable + +#### Funcionalidades de GUI + +- **Entrada numérica** por clicks en botones (#22, #31) +- **Botón decimal (.)** con validación para evitar múltiples puntos (#25, #32) +- **Lógica de operadores** matemáticos: +, -, \*, /, ^ (#26, #33) +- **Botones de control**: + - Clear (C): Limpia display y resetea estado (#28, #34) + - Backspace (⌫): Elimina último carácter (#28, #34) +- **Funciones científicas** integradas en GUI (#30, #35): + - Valor absoluto (abs) + - Máximo (max) + - Mínimo (min) +- **Soporte para números negativos** (#41, #45) + - Ingreso de números negativos en la GUI + - Manejo correcto de operaciones con negativos +- **Mejoras en manejo de múltiples números negativos** (#43) +- **Soporte completo de teclado** (#37, #47) + - Atajos para todos los números (0-9) + - Atajos para operadores (+, -, \*, /, ^) + - Enter o = para calcular resultado + - Escape para limpiar display + - Backspace para borrar último carácter + - Control completo mediante teclado o mouse + +#### Testing Automatizado + +- **Archivo `test_gui_calculator.py`** con suite completa de tests para GUI (#23, #48) + - Tests de clicks en botones numéricos + - Tests de botón Clear y Backspace + - Tests de operaciones básicas + - Tests de manejo de errores +- **Archivo `conftest.py`** con fixtures y mocks de Tkinter (#48) + - Clases dummy: DummyRoot, DummyEntry, DummyButton, DummyLabel + - Fixture `autouse` para sustituir componentes gráficos + - Tests ejecutables sin display gráfico (ideal para CI/CD) + - Compatibilidad con entornos headless + +#### CI/CD con GitHub Actions + +- **Pipeline automático** de integración continua (#36, #42) + - Workflow configurado para ramas `main` y `dev` + - Ejecución automática de tests en cada PR + - Validación continua de calidad de código + - Tests en ambiente headless sin Tcl/Tk + - Archivo `.github/workflows/ci.yml` configurado + +#### Documentación + +- **Guía de usuario** para la interfaz gráfica (#24, #40) + - Instrucciones de uso de la GUI + - Ejemplos de operaciones + - Atajos de teclado documentados +- **Mejoras en documentación** de funciones y manejo de errores (#29, #39) + +### Mejorado + +- **Experiencia de usuario** con dos interfaces disponibles: + - CLI (`main.py`): Interfaz de línea de comandos original + - GUI (`gui.py`): Interfaz gráfica moderna +- **Refactoring de lógica** redundante en manejo de operadores (#46) +- **Manejo visual de errores** en la GUI + - División por cero detectada y manejada + - Mensajes de error claros en el display + - Validación de entrada de decimales y negativos +- **Organización del proyecto** con separación clara CLI/GUI/tests +- **Calidad de código** con validación continua + +### Técnico + +- Implementación de clase `CalculatorGUI` con tkinter +- Sistema de grid layout responsivo para botones +- Binding de eventos de teclado en tkinter +- Fixtures de pytest con `autouse=True` +- Mocks de componentes Tkinter para testing sin GUI +- Workflow de GitHub Actions para CI/CD +- Integración completa entre GUI y módulo `calculator.py` + +### Mantenido + +- Interfaz de línea de comandos (CLI) en `main.py` +- Todas las funciones matemáticas originales +- Compatibilidad con Python 3.12+ +- Suite de tests unitarios (`test_calculator.py`) +- Templates de Issues y Pull Requests + +### Issues y PRs Incluidas + +**Issues Completadas:** + +- #15 - Implementar: Prototipo inicial de GUI +- #18 - Mejorar diseño: Ajustes y refinamientos de la GUI +- #20 - Mejora: Estilos visuales de la GUI +- #22 - Agregar función: Clicks de botones numéricos en GUI +- #23 - Pruebas: Testing manual completo de GUI +- #24 - Documentación: Guía de usuario para GUI +- #25 - Agregar función: Botón punto decimal en GUI +- #26 - Agregar función: Lógica de operadores en GUI +- #28 - Agregar funcionalidad: Botones Clear y Backspace +- #29 - Mejora: Manejo de errores y validaciones en GUI +- #30 - Agregar función: Funciones científicas en GUI +- #36 - Configurar CI/CD con GitHub Actions +- #37 - Agregar soporte de teclado para calculadora GUI +- #41 - Mejora: Números negativos sin operación de resta +- #43 - Error: Difícil ingresar múltiples números negativos + +**Pull Requests Mergeados:** + +- #48 - test: agregar tests de GUI con mocks de tkinter +- #47 - feat: agregar soporte de teclado para calculadora GUI +- #46 - refactor(gui): eliminar lógica redundante en el manejo de operadores +- #45 - feat(gui): soporte para números negativos y actualización de documentación +- #42 - feat: Configurar CI/CD con GitHub Actions +- #40 - docs: Añadir guía de usuario para la GUI +- #39 - refactor(gui): mejorar funciones unarias y manejo de errores +- #35 - feat: Agregar funciones científicas (abs, max, min) con integración en GUI +- #34 - feat: implementar botones C y ⌫ con su funcionalidad correspondiente +- #33 - feat(core): añadir lógica de operaciones (+, -, \*, /, ^) y soporte para botón "=" +- #32 - feat: implementar botón decimal con validación en la calculadora Tkinter +- #31 - feat: Implementar lógica de clic para entrada numérica +- #21 - feat: actualizar colores en gui.py +- #19 - feat: ajustar diseño según selección del equipo +- #17 - feat: Merge prototype calculator design from dev to main +- #16 - feat: agregar prototipo de GUI con diseño base + +### Agradecimientos + +Este release fue posible gracias a las contribuciones de: + +- **@Jandres25** (Jose Andres Meneces Lopez) + + - Coordinador del release + - Prototipo de GUI (#15, #16) + - CI/CD con GitHub Actions (#42) + - Testing automatizado de GUI (#23, #48) + - Soporte de teclado (#47) + +- **@Jhos3ph** + + - Funciones científicas en GUI (#35) + - Lógica de operaciones (#26, #33) + - Entrada numérica (#22, #31) + - Refactoring y optimizaciones (#39, #46) + - Soporte de números negativos (#45) + +- **@alexricardotapiacarita-ai** + - Diseño y estilos visuales (#18, #19, #20, #21) + - Botón decimal con validación (#25, #32) + - Botones Clear y Backspace (#28, #34) + - Documentación y guía de usuario (#24, #40) + +--- + ## [1.0.0] - 2025-11-04 ### Agregado @@ -28,11 +199,16 @@ y este proyecto se adhiere a [Semantic Versioning](https://semver.org/spec/v2.0. - Estructura del proyecto organizada - Sistema de pruebas configurado con pytest -## [Próximamente] +--- + +## [Próximamente] - v2.1.0 ### Planeado -- Interfaz gráfica usando tkinter -- Más operaciones matemáticas +- 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 -- Soporte para expresiones matemáticas complejas +- Más funciones matemáticas (raíz cuadrada, logaritmos, trigonometría) +- Temas personalizables (claro/oscuro) +- Exportar historial de cálculos diff --git a/README.md b/README.md index ad0eab5..67548c5 100644 --- a/README.md +++ b/README.md @@ -1,83 +1,200 @@ -# Team Practice - Flujo de Trabajo Colaborativo +# Team Practice - Calculadora con GUI 🧮✨ -Este repositorio está diseñado para practicar y aprender el flujo de trabajo colaborativo en equipo usando **Python** como lenguaje principal, elegido por su facilidad de uso y aprendizaje. +[![CI/CD](https://github.com/WorkTeam01/team-practice/actions/workflows/ci.yml/badge.svg)](https://github.com/WorkTeam01/team-practice/actions/workflows/ci.yml) +[![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. + +--- ## 🎯 Propósito del Proyecto -- **Practicar Git Flow**: Ramas, merge requests, resolución de conflictos +- **Practicar Git Flow**: Ramas, pull requests, resolución de conflictos - **Aprender colaboración**: Code reviews, pair programming, comunicación efectiva - **Desarrollar en Python**: Aprovechar la simplicidad del lenguaje para enfocarse en las prácticas de trabajo en equipo -- **Establecer buenas prácticas**: Documentación, testing, estructura de proyecto +- **Establecer buenas prácticas**: Documentación, testing, CI/CD, estructura de proyecto + +--- -## 🚀 Configuración Inicial +## ✨ Características v2.0.0 -### Prerrequisitos +### 🖥️ Interfaz Gráfica (GUI) -- Python 3.12 o superior -- Git -- Editor de código (VS Code recomendado) +- **Calculadora visual moderna** con tkinter +- **Tema oscuro profesional** con diseño elegante +- **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** + +### ⌨️ Atajos de Teclado -### Próximas Características +| Tecla | Acción | +| ------------------- | ----------------------- | +| `0-9` | Ingresar dígitos | +| `. ` | Punto decimal | +| `+` `-` `*` `/` `^` | Operaciones matemáticas | +| `Enter` o `=` | Calcular resultado | +| `Escape` | Limpiar display (Clear) | +| `Backspace` | Borrar último carácter | -- Interfaz gráfica con tkinter -- Más operaciones matemáticas -- Historial de operaciones -- Soporte para expresiones matemáticas complejas +### 💻 Interfaz de Línea de Comandos (CLI) -Para ver el historial detallado de cambios, consulta el archivo [CHANGELOG.md](CHANGELOG.md) +- Interfaz interactiva en terminal +- Todas las operaciones matemáticas disponibles +- Manejo robusto de errores + +### 🧪 Testing Automatizado + +- Tests unitarios con **pytest** +- Tests de GUI con mocks de Tkinter +- Ejecutable sin interfaz gráfica (headless) +- Ideal para CI/CD + +### 🤖 CI/CD con GitHub Actions + +- Ejecución automática de tests en cada PR +- Validación continua de calidad de código +- Pipeline configurado para `main` y `dev` + +--- + +## 🚀 Instalación y Uso + +### Prerrequisitos + +- Python 3.12 o superior +- pip (gestor de paquetes de Python) ### Instalación ```bash # Clonar el repositorio -git clone +git clone https://github.com/WorkTeam01/team-practice.git cd team-practice -# Instalar pytest (única dependencia necesaria) +# Instalar dependencias pip install pytest ``` +### Ejecutar la Aplicación + +#### Interfaz Gráfica (GUI) + +```bash +python gui.py +``` + +#### Interfaz de Línea de Comandos (CLI) + +```bash +python main.py +``` + +### Ejecutar Tests + +```bash +# Ejecutar todos los tests +pytest -v + +# Tests de calculadora básica +pytest test_calculator.py -v + +# Tests de GUI +pytest test_gui_calculator.py -v + +# Tests con cobertura +pytest --cov=. -v +``` + +--- + ## 📁 Estructura del Proyecto ``` team-practice/ -├── calculator.py # Código fuente principal -├── test_calculator.py # Pruebas unitarias -├── main.py # Programa principal -├── .gitignore # Archivos ignorados por Git -├── .github/ # Configuración y templates de GitHub -│ ├── ISSUE_TEMPLATE/ # Plantillas para issues -│ ├── PULL_REQUEST_TEMPLATE/ # Plantillas para pull requests -│ ├── pull_request_template.md # Template para pull requests -│ └── REVIEW_COMMENTS.md # Plantillas para comentarios de revisión -├── README.md # Este archivo -├── CHANGELOG.md # Historial de cambios -└── LICENSE # Licencia del proyecto +├── 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) +├── .github/ +│ ├── workflows/ +│ │ └── ci.yml # Pipeline de CI/CD +│ ├── ISSUE_TEMPLATE/ # Plantillas para issues +│ ├── PULL_REQUEST_TEMPLATE/ # Plantillas para PRs +│ ├── pull_request_template.md +│ └── REVIEW_COMMENTS.md +├── README.md # Este archivo +├── CHANGELOG.md # Historial de cambios +├── LICENSE # Licencia MIT +└── .gitignore # Archivos ignorados por Git ``` -## 🤝 Flujo de Trabajo +--- + +## 🧮 Operaciones Disponibles + +### Operaciones Básicas + +- ➕ **Suma**: `a + b` +- ➖ **Resta**: `a - b` +- ✖️ **Multiplicación**: `a * b` +- ➗ **División**: `a / b` +- 🔢 **Potencia**: `a ^ b` + +### Funciones Científicas + +- `abs(x)` - Valor absoluto +- `max(a, b)` - Valor máximo entre dos números +- `min(a, b)` - Valor mínimo entre dos números + +### Manejo de Errores + +- ⚠️ División por cero detectada y manejada +- 🛡️ Validación de entrada en ambas interfaces +- 📢 Mensajes de error claros + +--- + +## 🤝 Flujo de Trabajo Colaborativo ### 1. Antes de comenzar -- Hacer pull de la rama main -- Crear una nueva rama para tu feature: `git checkout -b feature/nombre-descriptivo` +```bash +# Actualizar rama main +git checkout main +git pull origin main + +# Crear rama para tu feature +git checkout -b feature/nombre-descriptivo +``` ### 2. Durante el desarrollo -- Commits frecuentes y descriptivos -- Seguir convenciones de naming -- Escribir tests para nuevas funcionalidades +- ✅ 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 -- Push de tu rama -- Crear Pull Request -- Solicitar code review -- Mergear después de aprobación +```bash +# Push de tu rama +git push origin feature/nombre-descriptivo + +# Crear Pull Request en GitHub +# Solicitar code review +# Mergear después de aprobación +``` + +--- ## 📋 Convenciones -### Commits +### Commits (Conventional Commits) ``` tipo: descripción breve @@ -85,82 +202,226 @@ tipo: descripción breve Descripción más detallada si es necesario Ejemplos: -feat: agregar función de validación de email -fix: corregir error en cálculo de descuentos -docs: actualizar README con instrucciones de setup +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 ``` +**Tipos de commit:** + +- `feat`: Nueva funcionalidad +- `fix`: Corrección de bug +- `docs`: Cambios en documentación +- `test`: Agregar o modificar tests +- `refactor`: Refactorización de código +- `style`: Cambios de formato (sin afectar lógica) +- `chore`: Tareas de mantenimiento + ### Ramas -- `main`: Rama principal (siempre estable) +- `main`: Rama principal (siempre estable, producción) +- `dev`: Rama de desarrollo (integración) - `feature/nombre-funcionalidad`: Nuevas características - `bugfix/descripcion-del-bug`: Corrección de errores -- `hotfix/descripcion-urgente`: Correcciones urgentes +- `hotfix/descripcion-urgente`: Correcciones urgentes en producción +- `release/vX.Y.Z`: Preparación de releases + +--- + +## 👥 Contribuir + +### 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`) + + ```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 + + ```bash + # Ejecutar tests localmente + pytest -v + ``` + +5. **Actualiza documentación** si es necesario + + - README.md + - Docstrings en el código + - CHANGELOG.md (si es un cambio significativo) + +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 + +7. **Espera code review** + - Responde a los comentarios + - Realiza los cambios solicitados +8. **Mergea** después de aprobación del equipo + +--- + +## 📦 Releases + +El proyecto sigue **[Versionamiento Semántico](https://semver.org/)**: + +### Versión Actual: **v2.0.0** 🎉 + +**Changelog completo:** + +- [CHANGELOG.md](CHANGELOG.md) - Historial detallado de todos los cambios + +**Versiones disponibles:** + +- **v2.0.0** (2025-11-28) - Interfaz gráfica + Testing + CI/CD +- **v1.0.0** (2025-11-04) - Calculadora CLI básica + +--- ## 🧪 Testing +### Ejecutar Tests + ```bash -# Ejecutar todas las pruebas -pytest test_calculator.py +# Todos los tests +pytest -v -# Ejecutar el programa principal -python main.py -``` +# Tests específicos +pytest test_calculator.py -v +pytest test_gui_calculator.py -v -## 👥 Contribuir +# Con cobertura +pytest --cov=. --cov-report=html -v -1. Asigna o crea un issue usando las plantillas proporcionadas - - Para bugs: usa la plantilla "reporte-error.md" - - Para nuevas funciones: usa la plantilla "funcion-calculadora.md" -2. Crea tu rama desde main -3. Implementa tu cambio -4. Agrega tests si aplica -5. Actualiza documentación -6. Crea Pull Request usando la plantilla de PR -7. Espera code review -8. Mergea después de aprobación +# Tests en modo verbose con detalles +pytest -vv +``` -### 📦 Releases +### Estructura de Tests -El proyecto sigue versionamiento semántico. Última versión estable: v1.0.0 +- **`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 -- Operaciones básicas: suma, resta, multiplicación, división, potencia -- Funciones adicionales: valor máximo, valor mínimo, valor absoluto -- Sistema de manejo de errores -- Interfaz de línea de comandos interactiva +--- ## 📞 Comunicación - **Issues**: Para reportar bugs o proponer features - **Pull Requests**: Para code reviews y discusión técnica - **Comentarios en código**: Para aclaraciones específicas +- **Discussions**: Para temas generales del proyecto + +--- -## 🔧 Comandos Útiles +## 🔧 Comandos Útiles de Git ```bash # Ver estado del repositorio git status # Ver historial de commits -git log --oneline +git log --oneline --graph -# Cambiar a rama main y actualizar -git checkout main && git pull +# Cambiar a rama dev y actualizar +git checkout dev && git pull origin dev -# Ver diferencias +# Ver diferencias antes de commit git diff # Agregar cambios y commitear -git add . && git commit -m "tu mensaje" +git add . +git commit -m "tipo(alcance): descripción" + +# Actualizar rama feature con cambios de dev +git checkout feature/mi-rama +git merge dev + +# Ver ramas locales y remotas +git branch -a + +# Eliminar rama local +git branch -d feature/mi-rama ``` -## 📚 Recursos de Aprendizaje +--- + +## 🎓 Recursos de Aprendizaje + +### Git y Flujo de Trabajo - [Git Flow Cheatsheet](https://danielkummer.github.io/git-flow-cheatsheet/) -- [Python Style Guide (PEP 8)](https://pep8.org/) -- [Python Style Guide (PEP 12)](https://www.python.org/dev/peps/pep-0012/) - [Conventional Commits](https://www.conventionalcommits.org/) +- [GitHub Flow](https://docs.github.com/en/get-started/quickstart/github-flow) + +### Python + +- [Python Style Guide (PEP 8)](https://pep8.org/) +- [Python Docstring Conventions (PEP 257)](https://peps.python.org/pep-0257/) +- [Tkinter Documentation](https://docs.python.org/3/library/tkinter.html) + +### Testing + +- [Pytest Documentation](https://docs.pytest.org/) +- [Testing Best Practices](https://docs.pytest.org/en/stable/goodpractices.html) --- -**¡Happy coding y colaboración efectiva!** 🐍✨ +## 🚧 Próximas Características (v2.1.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 + +--- + +## 🙏 Agradecimientos + +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 + +--- + +## 📄 Licencia + +Este proyecto está bajo la Licencia MIT. Ver [LICENSE](LICENSE) para más detalles. + +--- + +## 📊 Estadísticas del Proyecto + +- **Lenguaje**: Python 3.12+ +- **Framework GUI**: Tkinter +- **Framework Testing**: Pytest +- **CI/CD**: GitHub Actions +- **Commits**: 60+ +- **Pull Requests**: 24+ +- **Issues Cerradas**: 15+ + +--- + +**¡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/conftest.py b/conftest.py new file mode 100644 index 0000000..2800878 --- /dev/null +++ b/conftest.py @@ -0,0 +1,82 @@ +import pytest +import tkinter as tk + +class DummyRoot: + def __init__(self, *args, **kwargs): + self._title = "" + def title(self, t=None): + if t is not None: + self._title = t + return self._title + def geometry(self, *a, **k): + pass + def configure(self, *a, **k): + pass + def grid_rowconfigure(self, *a, **k): + pass + def grid_columnconfigure(self, *a, **k): + pass + def mainloop(self): + pass + def bind(self, *a, **k): + pass + def after(self, *a, **k): + pass + def destroy(self): + pass + +class DummyEntry: + def __init__(self, master=None, *args, **kwargs): + self._text = "" + def grid(self, *a, **k): + pass + def delete(self, start, end=None): + self._text = "" + def insert(self, index, text): + self._text = str(text) + # helper opcional para tests si quieren leer el contenido: + def get(self): + return self._text + +class DummyButton: + def __init__(self, master=None, *args, **kwargs): + pass + def grid(self, *a, **k): + pass + +class DummyLabel: + def __init__(self, master=None, *args, **kwargs): + self._text = kwargs.get('text', '') + def grid(self, *a, **k): + pass + def config(self, **kwargs): + if 'text' in kwargs: + self._text = kwargs['text'] + def get(self): + return self._text + +@pytest.fixture(autouse=True) +def replace_tkinter(): + """ + Fixture autouse que sustituye componentes gráficos por dummies durante los tests. + Esto evita la necesidad de tener Tcl/Tk instalado para ejecutar los tests unitarios. + """ + # Guardamos referencias originales para restaurar al finalizar el test + original_Tk = tk.Tk + original_Entry = tk.Entry + original_Button = tk.Button + original_Label = tk.Label + + tk.Tk = DummyRoot + tk.Entry = DummyEntry + tk.Button = DummyButton + tk.Label = DummyLabel + + try: + yield + finally: + # restaurar + tk.Tk = original_Tk + tk.Entry = original_Entry + tk.Button = original_Button + tk.Label = original_Label \ No newline at end of file diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md new file mode 100644 index 0000000..8711b9c --- /dev/null +++ b/docs/USER_GUIDE.md @@ -0,0 +1,204 @@ +# 📖 Guía de Usuario - Calculadora GUI + +## 🚀 Inicio Rápido + +### Requisitos + +- Python 3.12 o superior +- tkinter (incluido en Python estándar) + +### Ejecutar la aplicación + +```bash +python gui.py +``` + +![Vista Principal](https://raw.githubusercontent.com/WorkTeam01/team-practice/dev/docs/screenshots/calculator-main.jpeg) + +--- + +## 🖥️ Interfaz de Usuario + +### Display + +El display en la parte superior muestra: + +- Números ingresados +- Resultados de operaciones +- Mensajes de error + +### Botones Numéricos (0-9) + +Click en estos botones para ingresar números. + +**Ejemplo:** + +1. Click en "5" +2. Click en "2" +3. Display muestra: "52" + +### Botón Punto Decimal (.) + +Permite ingresar números decimales. + +**Ejemplo:** + +- Click en "5", ".", "2" → Display: "5.2" + +### Botones de Operadores (+, -, \*, /, ^) + +Realizan operaciones matemáticas. + +**Ejemplo de suma:** + +1. Ingresa "5" +2. Click en "+" +3. Ingresa "3" +4. Click en "=" +5. Resultado: "8" + +**Ejemplo de resta:** + +1. Ingresa "7" +2. Click en "-" +3. Ingresa "3" +4. Click en "=" +5. Resultado: "4" + +**Ejemplo de multipicación:** + +1. Ingresa "5" +2. Click en "\*" +3. Ingresa "5" +4. Click en "=" +5. Resultado: "25" + +**Ejemplo de división:** + +1. Ingresa "15" +2. Click en "/" +3. Ingresa "3" +4. Click en "=" +5. Resultado: "5" + +**Ejemplo de elevado:** + +1. Ingresa "5" +2. Click en "^" +3. Ingresa "3" +4. Click en "=" +5. Resultado: "125" + +![Operaciones basicas](https://raw.githubusercontent.com/WorkTeam01/team-practice/dev/docs/screenshots/operaciones-basicas.gif) + +--- + +## 🔬 Funciones Científicas + +### Valor Absoluto (abs) + +Convierte números negativos en positivos. + +**Ejemplo:** + +1. Ingresa "-5" +2. Click en "abs" +3. Resultado: "5" + +### Máximo (max) + +Encuentra el mayor de dos números. + +**Ejemplo:** + +1. Ingresa "10" +2. Click en "max" +3. Ingresa "20" +4. Click en "=" +5. Resultado: "20" + +### Mínimo (min) + +Encuentra el menor de dos números. + +**Ejemplo:** + +1. Ingresa "10" +2. Click en "min" +3. Ingresa "20" +4. Click en "=" +5. Resultado: "10" + +![Funciones Científicas](https://raw.githubusercontent.com/WorkTeam01/team-practice/dev/docs/screenshots/calculator-scientific.gif) + +--- + +## 🗑️ Botón Clear (C) + +Limpia el display y resetea la calculadora. + +**Uso:** + +- Click en "C" en cualquier momento +- Todo se resetea, listo para nuevo cálculo + +--- + +## ⚠️ Manejo de Errores + +### División por Cero + +La calculadora muestra un mensaje de error. + +**Ejemplo:** + +1. Ingresa "5" +2. Click en "/" +3. Ingresa "0" +4. Click en "=" +5. Display: "Error: No se puede dividir por cero" + +![Error](https://raw.githubusercontent.com/WorkTeam01/team-practice/dev/docs/screenshots/calculator-error.gif) + +**Solución:** Click en "C" para comenzar de nuevo + +--- + +## 💡 Consejos y Trucos + +### Operaciones Consecutivas + +Puedes continuar operando con el resultado: + +- `5 + 3 = 8` → `* 2 = 16` → `- 1 = 15` + +### Números Decimales + +- Puedes ingresar decimales en cualquier operación +- Solo un punto por número +- Ejemplo: `5.5 + 2.3 = 7.8` + +--- + +## 🆘 Troubleshooting + +### Problema: Display no muestra nada + +**Solución:** Click en "C" para resetear + +### Problema: Error después de operación + +**Solución:** Click en "C" y comienza de nuevo + +### Problema: No puedo ingresar más dígitos + +**Solución:** Probablemente alcanzaste el resultado, usa "C" o continúa operando + +--- + +## 📞 Soporte + +Si encuentras un bug o tienes sugerencias: + +- Crea un issue en GitHub +- Contacta al equipo de desarrollo diff --git a/docs/screenshots/calculator-error.gif b/docs/screenshots/calculator-error.gif new file mode 100644 index 0000000..291ad06 Binary files /dev/null and b/docs/screenshots/calculator-error.gif differ diff --git a/docs/screenshots/calculator-main.jpeg b/docs/screenshots/calculator-main.jpeg new file mode 100644 index 0000000..45659e5 Binary files /dev/null and b/docs/screenshots/calculator-main.jpeg differ diff --git a/docs/screenshots/calculator-scientific.gif b/docs/screenshots/calculator-scientific.gif new file mode 100644 index 0000000..031257f Binary files /dev/null and b/docs/screenshots/calculator-scientific.gif differ diff --git a/docs/screenshots/operaciones-basicas.gif b/docs/screenshots/operaciones-basicas.gif new file mode 100644 index 0000000..711a25f Binary files /dev/null and b/docs/screenshots/operaciones-basicas.gif differ diff --git a/gui.py b/gui.py index 10d8d15..6ada399 100644 --- a/gui.py +++ b/gui.py @@ -1,89 +1,429 @@ import tkinter as tk -from tkinter import ttk +from calculator import (add, subtract, multiply, divide, power, valor_maximo, valor_minimo, abs_value) class CalculatorGUI: def __init__(self, root): self.root = root self.root.title("Calculadora") - self.root.geometry("320x400") + self.root.geometry("330x450") + self.root.configure(bg="#1E1E1E") # Estado - self.current_value = "" - self.operator = None - self.first_number = None - - # UI + self.current_value = "" + self.operator = None + self.first_number = None + + # Interfaz de usuario self.create_widgets() + # Activar captura de teclado + self.root.bind('', self.handle_keypress) + def create_widgets(self): - # Display + + COLORS = { + "bg": "#1E1E1E", + "display": "#2B2B2B", + "numbers": "#3A3A3A", + "operators": "#FF8C00", + "equal": "#FF8C00", + "clear": "#6E6E6E", + "text": "#FFFFFF", + "error": "#FF4444" + } + + # Display principal self.display = tk.Entry( self.root, - font=('Arial', 24), - justify='right', - bg='white', - relief='sunken', - bd=3 + font=("Arial", 28, "bold"), + justify="right", + bg=COLORS["display"], + fg=COLORS["text"], + relief="flat", + bd=10 ) - self.display.grid(row=0, column=0, columnspan=4, sticky='nsew', padx=5, pady=5) - - # Estilo para los botones - style = ttk.Style() - # Un estilo simple para todos los botones - style.configure('Calculator.TButton', - font=('Arial', 14), - padding=5) - - # Definir botones y su posición + self.display.grid(row=0, column=0, columnspan=4, sticky="nsew", padx=10, pady=(10, 0)) + + # Label para mensajes de error + self.error_label = tk.Label( + self.root, + text="", + font=("Arial", 12), + bg=COLORS["bg"], + fg=COLORS["error"], + height=1 + ) + self.error_label.grid(row=1, column=0, columnspan=4, sticky="ew", padx=10, pady=(0, 5)) + + # Definición de botones buttons = [ - # Fila 1 - ('C', 1, 0), ('abs', 1, 1), ('max', 1, 2), ('min', 1, 3), - # Fila 2-5 - ('7', 2, 0), ('8', 2, 1), ('9', 2, 2), ('/', 2, 3), - ('4', 3, 0), ('5', 3, 1), ('6', 3, 2), ('*', 3, 3), - ('1', 4, 0), ('2', 4, 1), ('3', 4, 2), ('-', 4, 3), - ('0', 5, 0), ('.', 5, 1), ('=', 5, 2), ('+', 5, 3) + ('C', 2, 0, COLORS["clear"]), + ('(', 2, 1, COLORS["numbers"]), + (')', 2, 2, COLORS["numbers"]), + ('⌫', 2, 3, COLORS["clear"]), + + ('max', 3, 0, COLORS["numbers"]), + ('min', 3, 1, COLORS["numbers"]), + ('abs', 3, 2, COLORS["numbers"]), + ('/', 3, 3, COLORS["operators"]), + + ('7', 4, 0, COLORS["numbers"]), + ('8', 4, 1, COLORS["numbers"]), + ('9', 4, 2, COLORS["numbers"]), + ('*', 4, 3, COLORS["operators"]), + + ('4', 5, 0, COLORS["numbers"]), + ('5', 5, 1, COLORS["numbers"]), + ('6', 5, 2, COLORS["numbers"]), + ('-', 5, 3, COLORS["operators"]), + + ('1', 6, 0, COLORS["numbers"]), + ('2', 6, 1, COLORS["numbers"]), + ('3', 6, 2, COLORS["numbers"]), + ('+', 6, 3, COLORS["operators"]), + + ('^', 7, 0, COLORS["operators"]), + ('0', 7, 1, COLORS["numbers"]), + ('.', 7, 2, COLORS["numbers"]), + ('=', 7, 3, COLORS["equal"]), ] - - # Crear y posicionar botones - for (text, row, col) in buttons: - btn = ttk.Button( + + # Asignar función a cada botón + for (txt, r, c, color) in buttons: + if txt.isdigit(): + cmd = lambda t=txt: self.number_button_click(t) + elif txt == '.': + cmd = self.decimal_click + elif txt in ['+', '-', '*', '/', '^']: + cmd = lambda t=txt: self.operation_click(t) + elif txt == '=': + cmd = self.equals_click + elif txt == 'C': + cmd = self.clear_click + elif txt == '⌫': + cmd = self.backspace_click + elif txt in ['abs', 'max', 'min']: + cmd = lambda t=txt: self.scientific_click(t) + else: + cmd = lambda: None + + tk.Button( self.root, - text=text, - style='Calculator.TButton' - ) - btn.grid(row=row, column=col, padx=2, pady=2, sticky='nsew') - - # Configurar expansión de grid - for i in range(6): # 6 filas + text=txt, + bg=color, + fg=COLORS["text"], + font=("Arial", 16, "bold"), + relief="flat", + bd=0, + activebackground=color, + activeforeground=COLORS["text"], + highlightthickness=0, + command=cmd + ).grid(row=r, column=c, padx=4, pady=4, sticky="nsew") + + # Hacer que los botones se expandan con la ventana + for i in range(8): self.root.grid_rowconfigure(i, weight=1) - for i in range(4): # 4 columnas - self.root.grid_columnconfigure(i, weight=1) - - # Configurar expansión del root - self.root.grid_rowconfigure(1, weight=1) for i in range(4): self.root.grid_columnconfigure(i, weight=1) - def button_click(self, value): - """Maneja clicks de botones numéricos""" - pass + + def handle_keypress(self, event): + """Maneja las teclas presionadas por el usuario. + + Mapea las teclas del teclado a las funciones de la calculadora. + """ + key = event.char + + # Dígitos 0-9 + if key.isdigit(): + self.number_button_click(key) + + # Operadores básicos + elif key in ['+', '-', '*', '/']: + self.operation_click(key) + + # Potencia + elif key == '^': + self.operation_click('^') + + # Decimal + elif key == '.': + self.decimal_click() + + # Calcular (Enter o =) + elif key in ['\r', '\n', '=']: + self.equals_click() + + # Limpiar todo (Escape) + elif event.keysym == 'Escape': + self.clear_click() + + # Borrar último carácter (Backspace) + elif event.keysym == 'BackSpace': + self.backspace_click() + + + def number_button_click(self, valor): + """Maneja clicks de botones numéricos. + + Args: + value (str): Dígito presionado (0-9) + + Examples: + >>> # Usuario presiona 2, 3, 5 + >>> # Display muestra: "235" + """ + # Validar que sea un dígito + 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) + + + def decimal_click(self): + """Maneja click del botón decimal. + + 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 + + if '.' not in self.current_value: + if not self.current_value: + self.current_value = "0" + 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. + + Guarda el primer número y operador para calcular cuando + el usuario presione "=". - def operation_click(self, op): - """Maneja clicks de operadores""" - pass + Args: + operation (str): Operador (+, -, *, /, ^, max, min) + Examples: + >>> # Usuario: 5 + 3 = + >>> # 1. Ingresa "5" + >>> # 2. Click "+": first_number=5, operator="+" + >>> # 3. Ingresa "3" + >>> # 4. Click "=": calcula 5+3=8 + """ + # Permitir números negativos si se presiona '-' al inicio + if operation == '-' and (self.current_value == "" or self.current_value is None): + self.current_value = '-' + self.display.delete(0, tk.END) + self.display.insert(0, self.current_value) + return + if operation == '-' and self.current_value == '-': + return + + # Cambiar de operador si ya hay uno seleccionado + if not self.current_value: + if self.first_number is not None: + self.operator = operation + return + + # Validar que 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 + 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 + if self.first_number is not None and self.operator is not None: + self.equals_click() + try: + self.first_number = float(self.current_value) + except ValueError: + self.show_error("Número inválido") + return + self.operator = operation + self.current_value = "" + def equals_click(self): - """Calcula resultado usando calculator.py""" - pass + """Calcula el resultado de la operación actual. + + Usa las funciones de calculator. py para realizar el cálculo. + + Examples: + >>> # Usuario: 5 + 3 = + >>> # first_number=5, operator="+", current_value="3" + >>> # Ejecuta: add(5, 3) = 8 + >>> # Display: "8" + """ + 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 + elif self.first_number is not None and self.operator is not None and self.current_value: + try: + second_number = float(self.current_value) + result = None + + if self.operator == '+': + result = add(self.first_number, second_number) + elif self.operator == '-': + result = subtract(self.first_number, second_number) + elif self.operator == '*': + result = multiply(self.first_number, second_number) + elif self.operator == '/': + result = divide(self.first_number, second_number) + elif self.operator == '^': + result = power(self.first_number, second_number) + elif self.operator == 'max': + result = valor_maximo(self.first_number, second_number) + elif self.operator == 'min': + result = valor_minimo(self.first_number, second_number) + + self.display.delete(0, tk.END) + self.display.insert(0, str(result)) + self.current_value = str(result) + self.first_number = None + self.operator = None + + except ValueError: + self.show_error("Entrada inválida") + return + except ZeroDivisionError: + self.show_error("No se puede dividir por 0") + return + except Exception as e: + self.show_error(str(e)) + return + + def clear_click(self): - """Limpia display""" - pass + """Limpia completamente el display y resetea el estado de la calculadora. + + Resetea: + - current_value: cadena vacía + - operator: None + - first_number: None + - Display: vacío + + Examples: + >>> # Display muestra: "235" + >>> # Usuario presiona C + >>> # Display muestra: "" + """ + self.current_value = "" + self.operator = None + self.first_number = None + self.display.delete(0, tk.END) + self.error_label.config(text="") # Limpiar también el error + + + def backspace_click(self): + """Elimina el último carácter del display. + + 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: "" + """ + if 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. + + Args: + message (str): Mensaje de error a mostrar + + Examples: + >>> self.show_error("División por 0") + >>> # Label de error: "⚠️ División por 0" + """ + self.error_label.config(text=f"⚠️ {message}") + + # Limpiar error después de 3 segundos + self.root.after(3000, lambda: self.error_label.config(text="")) + + # Resetear estado + self.current_value = "" + self.first_number = None + self.operator = None + + # Limpiar el display también + self.display.delete(0, tk.END) + + + def unary_operation(self, func): + if self.current_value: + if self.current_value == "-": + self.show_error("Número incompleto") + return + try: + result = None + + if func == 'abs': + result = abs_value(float(self.current_value)) + #elif func == 'cos': + + self.display.delete(0, tk.END) + self.display.insert(0, str(result)) + self.current_value = str(result) + self.first_number = None + self.operator = None + + except Exception as e: + self.show_error(str(e)) + + def scientific_click(self, func): + """Maneja clicks de funciones científicas. + + Args: + func (str): Función científica (abs, max, min) + + Examples: + >>> # abs: Display "-5" → click "abs" → "5" + >>> # max: "10" → "max" → "20" → "=" → "20" + """ + + # Funciones que solo necesitan un número + if func in ['abs', 'cos', 'sin', 'tan']: + self.unary_operation(func) + # Funciones que necesitan dos números + elif func in ['max', 'min']: + self.operation_click(func) + def main(): root = tk.Tk() - app = CalculatorGUI(root) + app = CalculatorGUI(root) root.mainloop() if __name__ == "__main__": diff --git a/test_calculator.py b/test_calculator.py index c9d1c07..3d812e9 100644 --- a/test_calculator.py +++ b/test_calculator.py @@ -14,12 +14,14 @@ def test_add(): assert add(2, 3) == 5 assert add(-1, 1) == 0 assert add(0, 5) == 5 + assert add(-2, -3) == -5 def test_subtract(): """Test resta básica.""" assert subtract(5, 3) == 2 assert subtract(0, 5) == -5 + assert subtract(-2, -3) == 1 def test_multiply(): @@ -27,13 +29,15 @@ def test_multiply(): assert multiply(3, 4) == 12 assert multiply(5, 0) == 0 assert multiply(-2, 3) == -6 + assert multiply(-2, -3) == 6 def test_divide(): """Test división básica.""" assert divide(10, 2) == 5.0 assert divide(7, 2) == 3.5 - + assert divide(-2, 3) == -0.6666666666666666 + assert divide(-2, -3) == 0.6666666666666666 def test_divide_by_zero(): """Test que división por cero lanza excepción.""" @@ -46,6 +50,10 @@ def test_power(): assert power(2, 3) == 8 assert power(5, 0) == 1 assert power(4, 0.5) == 2.0 + 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" def test_valor_maximo(): diff --git a/test_gui_calculator.py b/test_gui_calculator.py new file mode 100644 index 0000000..c2a78e6 --- /dev/null +++ b/test_gui_calculator.py @@ -0,0 +1,718 @@ +"""Tests para la interfaz gráfica de la calculadora. + +Pruebas usando pytest para verificar funcionalidad de GUI. +Para correr los tests: pytest test_gui_calculator.py -v +""" +import tkinter as tk +from gui import CalculatorGUI + + +# ============================================================================ +# TESTS BÁSICOS +# ============================================================================ + +def test_initial_state(): + """Test del estado inicial de la calculadora.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + assert calc.current_value == "" + assert calc.operator is None + assert calc.first_number is None + + root.destroy() + + +def test_number_button_click(): + """Test de clicks en botones numéricos: simples y múltiples.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: Un solo dígito + calc.number_button_click("5") + assert calc.current_value == "5" + + # Caso 2: Múltiples dígitos + calc.number_button_click("3") + calc.number_button_click("7") + assert calc.current_value == "537" + + calc.clear_click() + + # Caso 3: Ceros al inicio + calc.number_button_click("0") + calc.number_button_click("0") + calc.number_button_click("5") + assert calc.current_value == "005" + + root.destroy() + + +def test_decimal_click(): + """Test del botón de punto decimal.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: Decimal con número + calc.number_button_click("3") + calc.decimal_click() + calc.number_button_click("5") + assert calc.current_value == "3.5" + + calc.clear_click() + + # Caso 2: Solo permite un punto decimal + calc.number_button_click("3") + calc.decimal_click() + calc.number_button_click("5") + calc.decimal_click() # Este no debería agregarse + assert calc.current_value == "3.5" + assert calc.current_value.count(".") == 1 + + calc.clear_click() + + # Caso 3: Decimal sin número (debe agregar 0) + calc.decimal_click() + assert calc.current_value == "0." + + root.destroy() + + +def test_clear_click(): + """Test del botón clear (C).""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Configurar estado con valores + calc.number_button_click("5") + calc.operation_click("+") + calc.number_button_click("3") + + # Limpiar todo + calc.clear_click() + + assert calc.current_value == "" + assert calc.operator is None + assert calc.first_number is None + + root.destroy() + + +def test_backspace_click(): + """Test del botón backspace (⌫).""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: Borrar dígitos normales + calc.number_button_click("1") + calc.number_button_click("2") + calc.number_button_click("3") + calc.backspace_click() + assert calc.current_value == "12" + + calc.backspace_click() + assert calc.current_value == "1" + + # Caso 2: Backspace con display vacío + calc.clear_click() + calc.backspace_click() + assert calc.current_value == "" + + # Caso 3: Borrar número negativo + calc.operation_click("-") + calc.number_button_click("5") + calc.backspace_click() + assert calc.current_value == "-" + + root.destroy() + + +def test_gui_structure(): + """Test de que los componentes GUI se crean correctamente.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Verificar que el display existe + assert calc.display is not None + + # Verificar título de ventana + assert calc.root.title() == "Calculadora" + + root.destroy() + + +# ============================================================================ +# TESTS DE OPERACIONES BÁSICAS +# ============================================================================ + +def test_suma(): + """Test de operación suma: positivos, negativos y mixtos.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: Suma básica positiva: 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() + + # Caso 2: Suma con negativo: -5 + 3 = -2 + calc.operation_click("-") + calc.number_button_click("5") + calc.operation_click("+") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == -2.0 + + calc.clear_click() + + # Caso 3: Suma de dos negativos: -5 + (-3) = -8 + calc.operation_click("-") + calc.number_button_click("5") + calc.operation_click("+") + calc.operation_click("-") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == -8.0 + + calc.clear_click() + + # Caso 4: Suma con cero: 0 + 5 = 5 + calc.number_button_click("0") + calc.operation_click("+") + calc.number_button_click("5") + calc.equals_click() + assert float(calc.current_value) == 5.0 + + root.destroy() + + +def test_resta(): + """Test de operación resta: positivos, negativos y mixtos.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: Resta básica: 10 - 4 = 6 + calc.number_button_click("1") + calc.number_button_click("0") + calc.operation_click("-") + calc.number_button_click("4") + calc.equals_click() + assert float(calc.current_value) == 6.0 + + calc.clear_click() + + # Caso 2: Resta que resulta en negativo: 3 - 10 = -7 + calc.number_button_click("3") + calc.operation_click("-") + calc.number_button_click("1") + calc.number_button_click("0") + calc.equals_click() + assert float(calc.current_value) == -7.0 + + calc.clear_click() + + # Caso 3: Resta con negativo: -5 - 3 = -8 + calc.operation_click("-") + 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() + + # Caso 4: Resta de negativos: 5 - (-3) = 8 + calc.number_button_click("5") + calc.operation_click("-") + calc.operation_click("-") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == 8.0 + + root.destroy() + + +def test_multiplicacion(): + """Test de operación multiplicación: positivos, negativos y cero.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: Multiplicación básica: 7 * 6 = 42 + calc.number_button_click("7") + calc.operation_click("*") + calc.number_button_click("6") + calc.equals_click() + assert float(calc.current_value) == 42.0 + + calc.clear_click() + + # Caso 2: Multiplicación por cero: 5 * 0 = 0 + calc.number_button_click("5") + calc.operation_click("*") + calc.number_button_click("0") + calc.equals_click() + assert float(calc.current_value) == 0.0 + + calc.clear_click() + + # Caso 3: Negativo * positivo: -2 * 3 = -6 + calc.operation_click("-") + calc.number_button_click("2") + calc.operation_click("*") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == -6.0 + + calc.clear_click() + + # Caso 4: Negativo * negativo: -2 * -3 = 6 + calc.operation_click("-") + calc.number_button_click("2") + calc.operation_click("*") + calc.operation_click("-") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == 6.0 + + root.destroy() + + +def test_division(): + """Test de operación división: positivos, negativos y error.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: División básica: 15 / 3 = 5 + calc.number_button_click("1") + calc.number_button_click("5") + calc.operation_click("/") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == 5.0 + + calc.clear_click() + + # Caso 2: División con decimales: 10 / 4 = 2.5 + calc.number_button_click("1") + calc.number_button_click("0") + calc.operation_click("/") + calc.number_button_click("4") + calc.equals_click() + assert float(calc.current_value) == 2.5 + + calc.clear_click() + + # Caso 3: División negativo/positivo: -10 / 2 = -5 + calc.operation_click("-") + 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) == -5.0 + + calc.clear_click() + + # Caso 4: División positivo/negativo: 10 / -2 = -5 + calc.number_button_click("1") + calc.number_button_click("0") + calc.operation_click("/") + calc.operation_click("-") + calc.number_button_click("2") + calc.equals_click() + assert float(calc.current_value) == -5.0 + + root.destroy() + + +def test_potencia(): + """Test de operación potencia: positivos y negativos.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: Potencia básica: 2 ^ 3 = 8 + calc.number_button_click("2") + calc.operation_click("^") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == 8.0 + + calc.clear_click() + + # Caso 2: Potencia con base negativa: -2 ^ 3 = -8 + calc.operation_click("-") + calc.number_button_click("2") + calc.operation_click("^") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == -8.0 + + calc.clear_click() + + # Caso 3: Potencia decimal (raíz): 4 ^ 0.5 = 2 + calc.number_button_click("4") + calc.operation_click("^") + calc.number_button_click("0") + calc.decimal_click() + calc.number_button_click("5") + calc.equals_click() + assert float(calc.current_value) == 2.0 + + calc.clear_click() + + # Caso 4: Potencia a cero: 5 ^ 0 = 1 + calc.number_button_click("5") + calc.operation_click("^") + calc.number_button_click("0") + calc.equals_click() + assert float(calc.current_value) == 1.0 + + root.destroy() + + +# ============================================================================ +# TESTS DE FUNCIONES CIENTÍFICAS +# ============================================================================ + +def test_valor_absoluto(): + """Test de valor absoluto: negativos, positivos y cero.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: abs de negativo: abs(-5) = 5 + calc.operation_click("-") + calc.number_button_click("5") + calc.scientific_click("abs") + assert float(calc.current_value) == 5.0 + + calc.clear_click() + + # Caso 2: abs de positivo: abs(3) = 3 + calc.number_button_click("3") + calc.scientific_click("abs") + assert float(calc.current_value) == 3.0 + + calc.clear_click() + + # Caso 3: abs de decimal negativo: abs(-2.5) = 2.5 + calc.operation_click("-") + calc.number_button_click("2") + calc.decimal_click() + calc.number_button_click("5") + calc.scientific_click("abs") + assert float(calc.current_value) == 2.5 + + calc.clear_click() + + # Caso 4: abs de cero: abs(0) = 0 + calc.number_button_click("0") + calc.scientific_click("abs") + assert float(calc.current_value) == 0.0 + + root.destroy() + + +def test_valor_maximo(): + """Test de valor máximo: positivos, negativos y mixtos.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: max de positivos: max(10, 20) = 20 + calc.number_button_click("1") + calc.number_button_click("0") + calc.scientific_click("max") + calc.number_button_click("2") + calc.number_button_click("0") + calc.equals_click() + assert float(calc.current_value) == 20.0 + + calc.clear_click() + + # Caso 2: max con negativos: max(-5, -10) = -5 + calc.operation_click("-") + calc.number_button_click("5") + calc.scientific_click("max") + calc.operation_click("-") + calc.number_button_click("1") + calc.number_button_click("0") + calc.equals_click() + assert float(calc.current_value) == -5.0 + + calc.clear_click() + + # Caso 3: max negativo y positivo: max(-2, 3) = 3 + calc.operation_click("-") + calc.number_button_click("2") + calc.scientific_click("max") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == 3.0 + + calc.clear_click() + + # Caso 4: max de iguales: max(4, 4) = 4 + calc.number_button_click("4") + calc.scientific_click("max") + calc.number_button_click("4") + calc.equals_click() + assert float(calc.current_value) == 4.0 + + root.destroy() + + +def test_valor_minimo(): + """Test de valor mínimo: positivos, negativos y mixtos.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: min de positivos: min(10, 20) = 10 + calc.number_button_click("1") + calc.number_button_click("0") + calc.scientific_click("min") + calc.number_button_click("2") + calc.number_button_click("0") + calc.equals_click() + assert float(calc.current_value) == 10.0 + + calc.clear_click() + + # Caso 2: min con negativos: min(-5, -10) = -10 + calc.operation_click("-") + calc.number_button_click("5") + calc.scientific_click("min") + calc.operation_click("-") + calc.number_button_click("1") + calc.number_button_click("0") + calc.equals_click() + assert float(calc.current_value) == -10.0 + + calc.clear_click() + + # Caso 3: min negativo y positivo: min(-2, 3) = -2 + calc.operation_click("-") + calc.number_button_click("2") + calc.scientific_click("min") + calc.number_button_click("3") + calc.equals_click() + assert float(calc.current_value) == -2.0 + + calc.clear_click() + + # Caso 4: min de iguales: min(7, 7) = 7 + calc.number_button_click("7") + calc.scientific_click("min") + calc.number_button_click("7") + calc.equals_click() + assert float(calc.current_value) == 7.0 + + root.destroy() + + +# ============================================================================ +# TESTS DE CASOS EXTREMOS Y ERRORES +# ============================================================================ + +def test_division_por_cero(): + """Test de división por cero: debe mostrar error.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + calc.number_button_click("5") + calc.operation_click("/") + calc.number_button_click("0") + calc.equals_click() + + # Después del error, debería limpiar el estado + assert calc.current_value == "" + assert calc.operator is None + assert calc.first_number is None + + root.destroy() + + +def test_operaciones_consecutivas(): + """Test de operaciones encadenadas sin presionar igual.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # 5 + 3 + 2 = 10 (presionando + después de 3 debe calcular 5+3) + calc.number_button_click("5") + calc.operation_click("+") + calc.number_button_click("3") + calc.operation_click("+") # Debe calcular 5+3=8 primero + calc.number_button_click("2") + calc.equals_click() + + assert float(calc.current_value) == 10.0 + + root.destroy() + + +def test_decimales(): + """Test de operaciones con decimales: positivos y negativos.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: Suma con decimales: 5.5 + 2.3 = 7.8 + calc.number_button_click("5") + calc.decimal_click() + calc.number_button_click("5") + calc.operation_click("+") + calc.number_button_click("2") + calc.decimal_click() + calc.number_button_click("3") + calc.equals_click() + result = float(calc.current_value) + assert abs(result - 7.8) < 0.0001 + + calc.clear_click() + + # Caso 2: Multiplicación con decimal negativo: -2.5 * 2 = -5 + calc.operation_click("-") + calc.number_button_click("2") + calc.decimal_click() + calc.number_button_click("5") + calc.operation_click("*") + calc.number_button_click("2") + calc.equals_click() + assert float(calc.current_value) == -5.0 + + calc.clear_click() + + # Caso 3: División con decimales: 10.5 / 2 = 5.25 + 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.equals_click() + assert float(calc.current_value) == 5.25 + + root.destroy() + + +def test_numeros_negativos(): + """Test de manejo de números negativos al inicio.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # Caso 1: Ingresar número negativo directamente: -5 + calc.operation_click("-") + calc.number_button_click("5") + assert calc.current_value == "-5" + + calc.clear_click() + + # Caso 2: Número negativo decimal: -3.14 + calc.operation_click("-") + calc.number_button_click("3") + calc.decimal_click() + calc.number_button_click("1") + calc.number_button_click("4") + assert calc.current_value == "-3.14" + + calc.clear_click() + + # Caso 3: Solo signo menos (incompleto) + abs debe dar error + calc.operation_click("-") + calc.scientific_click("abs") + # Debe mostrar error + assert calc.current_value == "" + + root.destroy() + + +def test_equals_sin_operacion(): + """Test de presionar = sin operación previa.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + calc.number_button_click("5") + calc.equals_click() + + # No debería crashear, mantiene el número + assert calc.current_value == "5" + + root.destroy() + + +def test_equals_sin_segundo_numero(): + """Test de presionar = sin segundo número.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + calc.number_button_click("5") + calc.operation_click("+") + calc.equals_click() + + # Debería mostrar error y limpiar + assert calc.current_value == "" + + root.destroy() + + +def test_resultado_como_operando(): + """Test de usar resultado en nueva operación: (5 + 3) * 2 = 16.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # 5 + 3 = 8 + calc.number_button_click("5") + calc.operation_click("+") + calc.number_button_click("3") + calc.equals_click() + + # 8 * 2 = 16 + calc.operation_click("*") + calc.number_button_click("2") + calc.equals_click() + + assert float(calc.current_value) == 16.0 + + root.destroy() + + +def test_clear_durante_operacion(): + """Test de limpiar en medio de una operación.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + calc.number_button_click("5") + calc.operation_click("+") + calc.number_button_click("3") + calc.clear_click() + + # Todo debería estar limpio + assert calc.current_value == "" + assert calc.operator is None + assert calc.first_number is None + + root.destroy() + + +def test_numeros_grandes(): + """Test con números grandes.""" + root = tk.Tk() + calc = CalculatorGUI(root) + + # 99999 * 99999 = 9999800001 + for _ in range(5): + calc.number_button_click("9") + + calc.operation_click("*") + + for _ in range(5): + calc.number_button_click("9") + + calc.equals_click() + + # Solo verificamos que no crashee + assert calc.current_value != "" + assert float(calc.current_value) == 9999800001.0 + + root.destroy() \ No newline at end of file