feat: added SPA support and speed up tests
CI / test (push) Successful in 1m2s
CI / build (push) Successful in 22s

This commit is contained in:
2026-09-16 16:16:34 +02:00
parent a462c1595d
commit e76b4b7a9c
15 changed files with 358 additions and 26 deletions
+17
View File
@@ -18,10 +18,27 @@ from django.conf import settings
from django.conf.urls.i18n import i18n_patterns from django.conf.urls.i18n import i18n_patterns
from django.contrib import admin from django.contrib import admin
from django.urls import include, path from django.urls import include, path
from django.views.generic import TemplateView
from django.views.static import serve from django.views.static import serve
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
# Sin prefijo de idioma (fuera de i18n_patterns, ver más abajo) ni bajo
# STATIC_URL: sw.js necesita servirse desde la raíz para que su ámbito
# (qué rutas puede interceptar un Service Worker) cubra el origen
# entero, no solo /static/ o /es/ (ver crochet/templates/sw.js). Se
# renderiza como plantilla (no un archivo estático de whitenoise) para
# que {% static %}/{% url %} resuelvan ahí mismo las rutas (con hash)
# vigentes en cada respuesta.
path(
'sw.js', name='service_worker',
view=TemplateView.as_view(template_name='sw.js', content_type='application/javascript'),
),
path(
'manifest.json', name='manifest',
view=TemplateView.as_view(template_name='manifest.json', content_type='application/manifest+json'),
),
path('offline/', name='offline', view=TemplateView.as_view(template_name='offline.html')),
] ]
# i18n_patterns antepone el prefijo de idioma (/es/..., /en/...) a cada URL, # i18n_patterns antepone el prefijo de idioma (/es/..., /en/...) a cada URL,
+15
View File
@@ -6,6 +6,21 @@ from django.core.management import call_command
from django.test import override_settings from django.test import override_settings
@pytest.fixture(scope='session', autouse=True)
def fast_password_hasher():
"""PBKDF2PasswordHasher (por defecto, ver AUTH_PASSWORD_VALIDATORS) es
deliberadamente lento -es justo lo que lo hace seguro en producción-,
pero eso mismo hace bastante caro cada create_user()/check_password()/
login de los tests: con cientos de ellos (y los de rate limiting, que
encima repiten el login 10-11 veces seguidas) esto es la mayor parte
del tiempo de toda la suite. MD5PasswordHasher no vale para producción,
pero aquí no protege nada real -las contraseñas son literales fijos
como 'a-very-uncommon-pw-1'-, así que no hay ninguna pérdida de
seguridad en usarlo solo para tests."""
with override_settings(PASSWORD_HASHERS=['django.contrib.auth.hashers.MD5PasswordHasher']):
yield
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def clear_ratelimit_cache(): def clear_ratelimit_cache():
"""django-ratelimit (ver LOGIN_RATE/REGISTER_RATE/PASSWORD_RESET_RATE en """django-ratelimit (ver LOGIN_RATE/REGISTER_RATE/PASSWORD_RESET_RATE en
+31 -15
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: crochet\n" "Project-Id-Version: crochet\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-16 11:16+0000\n" "POT-Creation-Date: 2026-09-16 14:05+0000\n"
"Language: en\n" "Language: en\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -46,7 +46,7 @@ msgstr ""
"made in a row from here." "made in a row from here."
#: crochet/templates/403.html:24 crochet/templates/404.html:19 #: crochet/templates/403.html:24 crochet/templates/404.html:19
#: crochet/templates/500.html:64 #: crochet/templates/500.html:65
#: crochet/templates/crochet/cookie_policy.html:54 #: crochet/templates/crochet/cookie_policy.html:54
msgid "Volver al inicio" msgid "Volver al inicio"
msgstr "Back to home" msgstr "Back to home"
@@ -63,11 +63,11 @@ msgstr "The page you're looking for doesn't exist or has moved."
msgid "Error del servidor" msgid "Error del servidor"
msgstr "Server error" msgstr "Server error"
#: crochet/templates/500.html:62 #: crochet/templates/500.html:63
msgid "Vaya, algo ha ido mal" msgid "Vaya, algo ha ido mal"
msgstr "Oops, something went wrong" msgstr "Oops, something went wrong"
#: crochet/templates/500.html:63 #: crochet/templates/500.html:64
msgid "Ha ocurrido un error inesperado. Prueba a recargar en un momento." msgid "Ha ocurrido un error inesperado. Prueba a recargar en un momento."
msgstr "An unexpected error occurred. Try reloading in a moment." msgstr "An unexpected error occurred. Try reloading in a moment."
@@ -225,9 +225,8 @@ msgid "Actualizado el %(date)s"
msgstr "Updated on %(date)s" msgstr "Updated on %(date)s"
#: crochet/templates/crochet/account_home.html:44 #: crochet/templates/crochet/account_home.html:44
#: crochet/templates/crochet/pattern.html:156 msgid "Editar"
msgid "Vista previa" msgstr "Edit"
msgstr "Preview"
#: crochet/templates/crochet/account_home.html:48 #: crochet/templates/crochet/account_home.html:48
#: crochet/templates/crochet/pattern.html:46 #: crochet/templates/crochet/pattern.html:46
@@ -240,7 +239,7 @@ msgstr "You don't have any patterns yet."
#: crochet/templates/crochet/account_settings.html:4 #: crochet/templates/crochet/account_settings.html:4
#: crochet/templates/crochet/account_settings.html:18 #: crochet/templates/crochet/account_settings.html:18
#: crochet/templates/crochet/base.html:62 #: crochet/templates/crochet/base.html:71
msgid "Ajustes de la cuenta" msgid "Ajustes de la cuenta"
msgstr "Account settings" msgstr "Account settings"
@@ -274,19 +273,19 @@ msgstr ""
msgid "Esto borrará tu cuenta y todos tus patrones de forma permanente." msgid "Esto borrará tu cuenta y todos tus patrones de forma permanente."
msgstr "This will permanently delete your account and all your patterns." msgstr "This will permanently delete your account and all your patterns."
#: crochet/templates/crochet/base.html:37 #: crochet/templates/crochet/base.html:46
msgid "Cambiar tema" msgid "Cambiar tema"
msgstr "Change theme" msgstr "Change theme"
#: crochet/templates/crochet/base.html:49 #: crochet/templates/crochet/base.html:58
msgid "Cambiar idioma" msgid "Cambiar idioma"
msgstr "Change language" msgstr "Change language"
#: crochet/templates/crochet/base.html:66 #: crochet/templates/crochet/base.html:75
msgid "Cerrar sesión" msgid "Cerrar sesión"
msgstr "Log out" msgstr "Log out"
#: crochet/templates/crochet/base.html:72 #: crochet/templates/crochet/base.html:81
#: crochet/templates/crochet/home.html:25 #: crochet/templates/crochet/home.html:25
#: crochet/templates/crochet/password_reset_complete.html:18 #: crochet/templates/crochet/password_reset_complete.html:18
#: crochet/templates/registration/login.html:4 #: crochet/templates/registration/login.html:4
@@ -758,6 +757,10 @@ msgstr "Landscape"
msgid "Secciones" msgid "Secciones"
msgstr "Sections" msgstr "Sections"
#: crochet/templates/crochet/pattern.html:156
msgid "Vista previa"
msgstr "Preview"
#: crochet/templates/crochet/pattern.html:166 #: crochet/templates/crochet/pattern.html:166
msgid "Idioma del patrón" msgid "Idioma del patrón"
msgstr "Pattern language" msgstr "Pattern language"
@@ -906,6 +909,22 @@ msgstr ""
msgid "Confirmar" msgid "Confirmar"
msgstr "Confirm" msgstr "Confirm"
#: crochet/templates/offline.html:20 crochet/templates/offline.html:70
msgid "Sin conexión"
msgstr "No connection"
#: crochet/templates/offline.html:71
msgid ""
"No se ha podido cargar esta página porque no tienes conexión a internet. "
"Comprueba tu conexión e inténtalo de nuevo."
msgstr ""
"This page couldn't be loaded because you're not connected to the internet. "
"Check your connection and try again."
#: crochet/templates/offline.html:72
msgid "Reintentar"
msgstr "Retry"
#: crochet/templates/registration/login.html:17 #: crochet/templates/registration/login.html:17
msgid "Usuario o contraseña incorrectos." msgid "Usuario o contraseña incorrectos."
msgstr "Incorrect username or password." msgstr "Incorrect username or password."
@@ -1109,8 +1128,5 @@ msgstr "Your account has been deleted successfully."
#~ msgid "Idioma" #~ msgid "Idioma"
#~ msgstr "Language" #~ msgstr "Language"
#~ msgid "Editar"
#~ msgstr "Edit"
#~ msgid "Eliminar patrón" #~ msgid "Eliminar patrón"
#~ msgstr "Delete pattern" #~ msgstr "Delete pattern"
+28 -11
View File
@@ -2,7 +2,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: crochet\n" "Project-Id-Version: crochet\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-16 11:16+0000\n" "POT-Creation-Date: 2026-09-16 14:05+0000\n"
"Language: es\n" "Language: es\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
@@ -44,7 +44,7 @@ msgid ""
msgstr "" msgstr ""
#: crochet/templates/403.html:24 crochet/templates/404.html:19 #: crochet/templates/403.html:24 crochet/templates/404.html:19
#: crochet/templates/500.html:64 #: crochet/templates/500.html:65
#: crochet/templates/crochet/cookie_policy.html:54 #: crochet/templates/crochet/cookie_policy.html:54
msgid "Volver al inicio" msgid "Volver al inicio"
msgstr "" msgstr ""
@@ -61,11 +61,11 @@ msgstr ""
msgid "Error del servidor" msgid "Error del servidor"
msgstr "" msgstr ""
#: crochet/templates/500.html:62 #: crochet/templates/500.html:63
msgid "Vaya, algo ha ido mal" msgid "Vaya, algo ha ido mal"
msgstr "" msgstr ""
#: crochet/templates/500.html:63 #: crochet/templates/500.html:64
msgid "Ha ocurrido un error inesperado. Prueba a recargar en un momento." msgid "Ha ocurrido un error inesperado. Prueba a recargar en un momento."
msgstr "" msgstr ""
@@ -215,8 +215,7 @@ msgid "Actualizado el %(date)s"
msgstr "" msgstr ""
#: crochet/templates/crochet/account_home.html:44 #: crochet/templates/crochet/account_home.html:44
#: crochet/templates/crochet/pattern.html:156 msgid "Editar"
msgid "Vista previa"
msgstr "" msgstr ""
#: crochet/templates/crochet/account_home.html:48 #: crochet/templates/crochet/account_home.html:48
@@ -230,7 +229,7 @@ msgstr ""
#: crochet/templates/crochet/account_settings.html:4 #: crochet/templates/crochet/account_settings.html:4
#: crochet/templates/crochet/account_settings.html:18 #: crochet/templates/crochet/account_settings.html:18
#: crochet/templates/crochet/base.html:62 #: crochet/templates/crochet/base.html:71
msgid "Ajustes de la cuenta" msgid "Ajustes de la cuenta"
msgstr "" msgstr ""
@@ -262,19 +261,19 @@ msgstr ""
msgid "Esto borrará tu cuenta y todos tus patrones de forma permanente." msgid "Esto borrará tu cuenta y todos tus patrones de forma permanente."
msgstr "" msgstr ""
#: crochet/templates/crochet/base.html:37 #: crochet/templates/crochet/base.html:46
msgid "Cambiar tema" msgid "Cambiar tema"
msgstr "" msgstr ""
#: crochet/templates/crochet/base.html:49 #: crochet/templates/crochet/base.html:58
msgid "Cambiar idioma" msgid "Cambiar idioma"
msgstr "" msgstr ""
#: crochet/templates/crochet/base.html:66 #: crochet/templates/crochet/base.html:75
msgid "Cerrar sesión" msgid "Cerrar sesión"
msgstr "" msgstr ""
#: crochet/templates/crochet/base.html:72 #: crochet/templates/crochet/base.html:81
#: crochet/templates/crochet/home.html:25 #: crochet/templates/crochet/home.html:25
#: crochet/templates/crochet/password_reset_complete.html:18 #: crochet/templates/crochet/password_reset_complete.html:18
#: crochet/templates/registration/login.html:4 #: crochet/templates/registration/login.html:4
@@ -718,6 +717,10 @@ msgstr ""
msgid "Secciones" msgid "Secciones"
msgstr "" msgstr ""
#: crochet/templates/crochet/pattern.html:156
msgid "Vista previa"
msgstr ""
#: crochet/templates/crochet/pattern.html:166 #: crochet/templates/crochet/pattern.html:166
msgid "Idioma del patrón" msgid "Idioma del patrón"
msgstr "" msgstr ""
@@ -860,6 +863,20 @@ msgstr ""
msgid "Confirmar" msgid "Confirmar"
msgstr "" msgstr ""
#: crochet/templates/offline.html:20 crochet/templates/offline.html:70
msgid "Sin conexión"
msgstr ""
#: crochet/templates/offline.html:71
msgid ""
"No se ha podido cargar esta página porque no tienes conexión a internet. "
"Comprueba tu conexión e inténtalo de nuevo."
msgstr ""
#: crochet/templates/offline.html:72
msgid "Reintentar"
msgstr ""
#: crochet/templates/registration/login.html:17 #: crochet/templates/registration/login.html:17
msgid "Usuario o contraseña incorrectos." msgid "Usuario o contraseña incorrectos."
msgstr "" msgstr ""
Binary file not shown.

After

Width:  |  Height:  |  Size: 490 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

+18
View File
@@ -0,0 +1,18 @@
// Registra el Service Worker de la PWA (ver crochet/templates/sw.js) para
// que la app sea instalable y tenga soporte offline básico. La ruta va a
// pelo ('/sw.js', no bajo /static/ ni con prefijo de idioma) porque tiene
// que coincidir exactamente con dónde lo sirve config/urls.py: el ámbito
// de un Service Worker es la carpeta de su propia URL y las de debajo, así
// que solo sirviéndolo (y registrándolo) desde la raíz cubre el origen
// entero en vez de quedarse limitado a /static/ o /es/.
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/sw.js').catch(() => {
// Sin conexión, en un navegador sin soporte real pese a exponer el
// objeto, o serviceWorker.register() con documento no seguro (http
// sin ser localhost): la app sigue funcionando con normalidad, solo
// sin lo que aporta la PWA (instalación, caché offline), así que no
// hace falta avisar a quien la usa.
});
});
}
+1
View File
@@ -14,6 +14,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'Error del servidor' %}</title> <title>{% trans 'Error del servidor' %}</title>
<link rel="stylesheet" href="{% static 'css/main.css' %}"> <link rel="stylesheet" href="{% static 'css/main.css' %}">
<link rel="icon" href="{% static 'img/favicon.ico' %}">
<style> <style>
body { body {
display: flex; display: flex;
+10
View File
@@ -18,6 +18,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Crochet{% endblock %}</title> <title>{% block title %}Crochet{% endblock %}</title>
<link rel="stylesheet" href="{% static 'css/main.css' %}"> <link rel="stylesheet" href="{% static 'css/main.css' %}">
<!-- PWA: instalable (manifest + iconos) y con soporte offline básico
(ver crochet/templates/sw.js, registrado al final de este documento).
pattern_detail.html (vista de solo lectura/PDF) hereda esto también:
no hace daño -no vuelve a registrar nada, el navegador ya lo tiene
activo- e instalar la PWA desde ahí sigue llevando al mismo shell. -->
<link rel="manifest" href="{% url 'manifest' %}">
<meta name="theme-color" content="#f1641e">
<link rel="icon" href="{% static 'img/favicon.ico' %}">
<link rel="apple-touch-icon" href="{% static 'img/icon-192.png' %}">
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
@@ -75,6 +84,7 @@
</nav> </nav>
{% endblock %} {% endblock %}
<script defer src="{% static 'js/theme-toggle.js' %}"></script> <script defer src="{% static 'js/theme-toggle.js' %}"></script>
<script defer src="{% static 'js/sw-register.js' %}"></script>
<!-- Mensajes de una sola vez tras una redirección (ver messages.success <!-- Mensajes de una sola vez tras una redirección (ver messages.success
en AccountDeleteView): no hay ninguna página a la que volver con el en AccountDeleteView): no hay ninguna página a la que volver con el
resultado de la acción (la sesión ya no existe), así que se muestran resultado de la acción (la sesión ya no existe), así que se muestran
+23
View File
@@ -0,0 +1,23 @@
{% load static %}{
"name": "Crochet",
"short_name": "Crochet",
"description": "Diseña patrones de crochet con un editor visual y expórtalos a PDF.",
"lang": "es",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#fffbf5",
"theme_color": "#f1641e",
"icons": [
{
"src": "{% static 'img/icon-192.png' %}",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "{% static 'img/icon-512.png' %}",
"sizes": "512x512",
"type": "image/png"
}
]
}
+76
View File
@@ -0,0 +1,76 @@
{% load i18n static %}
<!DOCTYPE html>
<!-- Autónoma, sin extender crochet/base.html: la sirve el Service Worker
(ver sw.js) como respuesta de reserva cuando falla una navegación sin
conexión, así que no puede depender de nada que a su vez necesite red
(el CDN de daisyUI/Tailwind, Google Fonts) ni de context processors -
mismo motivo que 500.html, que ya sigue este mismo patrón. -->
<html lang="es" data-theme="crochet">
<head>
<script>
try {
if (localStorage.getItem('theme') === 'crochet-dark') {
document.documentElement.dataset.theme = 'crochet-dark';
}
} catch {}
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans 'Sin conexión' %}</title>
<link rel="stylesheet" href="{% static 'css/main.css' %}">
<link rel="icon" href="{% static 'img/favicon.ico' %}">
<style>
body {
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
margin: 0;
padding: 1rem;
}
.box {
max-width: 24rem;
width: 100%;
text-align: center;
background: var(--brand-bg-soft);
border: 1px solid var(--brand-border);
border-radius: 1rem;
padding: 2rem;
}
.box h1 {
margin: 0 0 .5rem;
font-size: 1.25rem;
}
.box p {
margin: 0 0 1rem;
opacity: .7;
font-size: .9rem;
}
.box button {
border: none;
background-color: var(--brand-primary);
color: var(--brand-primary-content);
text-decoration: none;
font-weight: 600;
padding: .5rem 1.5rem;
border-radius: 999px;
cursor: pointer;
font-size: .9rem;
}
</style>
</head>
<body>
<div class="box">
<h1>{% trans 'Sin conexión' %}</h1>
<p>{% trans 'No se ha podido cargar esta página porque no tienes conexión a internet. Comprueba tu conexión e inténtalo de nuevo.' %}</p>
<button type="button" onclick="location.reload()">{% trans 'Reintentar' %}</button>
</div>
</body>
</html>
+96
View File
@@ -0,0 +1,96 @@
{% load static %}
// Service Worker de la PWA: soporte offline básico, no un cache completo de
// la aplicación (los patrones son contenido dinámico, guardado en base de
// datos, y no se cachean aquí). Se limita a dos cosas:
// 1. Cachear (cache-first) los estáticos propios de la app shell
// (main.css, theme.css, iconos), para que carguen al instante y sigan
// disponibles sin conexión.
// 2. Si una navegación (clic en un enlace, recargar la página) falla por
// no haber red, responder con la página de "sin conexión" (ver
// crochet/templates/offline.html) en vez del error genérico del
// navegador.
// Todo lo demás (peticiones a /media/, fetch de la propia app a
// pattern_save/pattern_image_upload, el CDN de daisyUI/Tailwind/Google
// Fonts) se deja pasar sin interceptar: ni se cachea ni se sirve un
// fallback, el navegador hace lo de siempre.
//
// Esta plantilla la sirve el TemplateView "service_worker" (ver
// config/urls.py) con content_type=application/javascript, no whitenoise:
// así el tag "static" de Django resuelve aquí el nombre real (con hash) de
// cada archivo en el momento de servir esta respuesta, en vez de tener que
// mantenerlo a mano.
//
// CACHE_NAME es fijo a propósito (no calculado a partir del hash de los
// estáticos): el propio hash cambia el contenido de este archivo cada vez
// que cambia main.css/theme.css, y los navegadores comparan el Service
// Worker byte a byte en cada visita para detectar actualizaciones -eso ya
// basta para que se reinstale solo y su "install" vuelva a poblar el cache
// con las URLs (con hash) nuevas-. Subir el número a mano de vez en cuando
// (aquí y en ningún otro sitio) sirve solo para forzar que "activate"
// limpie del todo cachés de una versión muy antigua, ver más abajo.
const CACHE_NAME = 'crochet-shell-v1';
const OFFLINE_URL = '{% url "offline" %}';
const APP_SHELL = [
OFFLINE_URL,
'{% static "css/main.css" %}',
'{% static "css/theme.css" %}',
'{% static "img/icon-192.png" %}',
'{% static "img/icon-512.png" %}',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(APP_SHELL))
// skipWaiting: sin esto, un Service Worker nuevo se queda en
// "waiting" hasta que se cierran todas las pestañas con el antiguo
// todavía activo, así que la próxima visita seguiría sirviendo el
// shell (y el offline.html) de la versión vieja.
.then(() => self.skipWaiting()),
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys()
.then((keys) => Promise.all(keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))))
.then(() => self.clients.claim()),
);
});
self.addEventListener('fetch', (event) => {
const { request } = event;
// mode 'navigate': una petición de navegación de verdad (clic en un
// enlace, recargar, escribir la URL), no un fetch/XHR de main.js/
// storage.js ni la petición de una imagen/script. Es justo lo único
// para lo que tiene sentido el fallback de offline.html.
if (request.mode === 'navigate') {
event.respondWith(
fetch(request).catch(() => caches.match(OFFLINE_URL)),
);
return;
}
const url = new URL(request.url);
// Mismo origen y bajo STATIC_URL únicamente: ni /media/ (portadas/
// imágenes de patrón, que pueden pesar bastante y no forman parte del
// "shell") ni el CDN externo de daisyUI/Tailwind/Google Fonts, que ya
// tiene su propio cache HTTP de navegador y no conviene duplicar aquí.
if (url.origin === self.location.origin && url.pathname.startsWith('/static/')) {
event.respondWith(
caches.match(request).then((cached) => {
if (cached) {
return cached;
}
return fetch(request).then((response) => {
const responseCopy = response.clone();
caches.open(CACHE_NAME).then((cache) => cache.put(request, responseCopy));
return response;
});
}),
);
}
});
+43
View File
@@ -0,0 +1,43 @@
from django.test import TestCase
from django.urls import reverse
class ServiceWorkerViewTests(TestCase):
def test_served_at_the_domain_root(self):
response = self.client.get('/sw.js')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/javascript')
def test_references_the_offline_page_and_own_static_assets(self):
response = self.client.get('/sw.js')
content = response.content.decode()
self.assertIn("OFFLINE_URL = '/offline/'", content)
self.assertIn('/static/css/main.', content)
self.assertIn('/static/css/theme.', content)
self.assertIn('/static/img/icon-192.', content)
self.assertIn('/static/img/icon-512.', content)
class ManifestViewTests(TestCase):
def test_served_with_the_manifest_content_type(self):
response = self.client.get('/manifest.json')
self.assertEqual(response.status_code, 200)
self.assertEqual(response['Content-Type'], 'application/manifest+json')
def test_references_both_icon_sizes(self):
response = self.client.get('/manifest.json')
self.assertContains(response, '/static/img/icon-192.')
self.assertContains(response, '/static/img/icon-512.')
class OfflineViewTests(TestCase):
def test_renders_the_offline_page(self):
response = self.client.get(reverse('offline'))
self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Sin conexión')