Skip to content

Commit cc92759

Browse files
Merge pull request #309 from RSE-Sheffield/feat/test-frontend
Add frontend testing framework
2 parents f72aa34 + 68e3c59 commit cc92759

25 files changed

+2311
-107
lines changed

.github/workflows/django-check.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ jobs:
2525
# This should match the version used in production
2626
node-version: 'v20.x'
2727
cache: 'npm'
28+
cache-dependency-path: 'package-lock.json'
2829
- name: Install JavaScript package
2930
run: |
3031
npm ci
Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,36 @@
11
# ESLint is a static code analysis tool for finding problems with ECMAScript/JavaScript code
22
# https://eslint.org/
3-
name: Lint JavaScript code
3+
name: Lint & test JavaScript code
44
on:
55
push:
6-
branches: [ "dev" ]
7-
pull_request:
86
branches: [ "dev" ]
7+
pull_request:
8+
branches: [ "dev", "main" ]
9+
permissions:
10+
contents: read
11+
pull-requests: write # post coverage reports
912
jobs:
10-
build:
13+
test:
1114
runs-on: ubuntu-24.04
1215
steps:
13-
- name: Checkout
14-
uses: actions/[email protected]
15-
- name: Setup Node.js
16-
uses: actions/setup-node@v4
17-
with:
18-
node-version: 'v20.x'
19-
cache: 'npm'
20-
cache-dependency-path: 'package-lock.json'
21-
- run: npm ci
22-
# https://eslint.org/docs/latest/use/command-line-interface
23-
# We also need the Svelte linter https://sveltejs.github.io/eslint-plugin-svelte/
24-
# This could be improved by using a configuration file
25-
# https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file
26-
- name: Run ESLint
27-
working-directory: .
28-
run: npm run lintjs
16+
- name: Checkout
17+
uses: actions/[email protected]
18+
- name: Setup Node.js
19+
uses: actions/setup-node@v4
20+
with:
21+
node-version-file: 'package.json'
22+
cache-dependency-path: 'package-lock.json'
23+
- run: npm ci
24+
# https://eslint.org/docs/latest/use/command-line-interface
25+
# We also need the Svelte linter https://sveltejs.github.io/eslint-plugin-svelte/
26+
# This could be improved by using a configuration file
27+
# https://eslint.org/docs/latest/use/configure/configuration-files#configuration-file
28+
- name: Run linter
29+
run: npm run lint
30+
- name: Run test suite
31+
run: npm run test:coverage
32+
- name: Report coverage
33+
uses: davelosert/[email protected]
34+
with:
35+
json-summary-path: ui_components/coverage/coverage-summary.json
36+
json-final-path: ui_components/coverage/coverage-final.json

SORT/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from SORT.checks import *

SORT/checks/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Django system checks
2+
3+
See: [System check framework](https://docs.djangoproject.com/en/5.2/topics/checks/) in the Django documentation.
4+
5+
To activate a system check, import it to `SORT/checks/__init__.py` and append the function name to the `__all__` list.
6+
This will ensure the check is registered when the application is initialised.

SORT/checks/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .vite_manifest import check_manifest
2+
from .survey_config import check_survey_config
3+
4+
__all__ = ["check_manifest", "check_survey_config"]

SORT/checks/survey_config.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from pathlib import Path
2+
from django.core.checks import Tags, Error
3+
import django.conf
4+
import django.contrib.staticfiles.finders
5+
6+
settings = django.conf.settings
7+
8+
SURVEY_CONFIG_FILE_PATHS = {
9+
"data/survey_config/consent_only_config.json",
10+
"data/survey_config/demography_only_config.json"
11+
}
12+
13+
14+
@django.core.checks.register(Tags.staticfiles)
15+
def check_survey_config(*args, **kwargs) -> list[Error]:
16+
"""
17+
Ensure that survey configuration files exist
18+
"""
19+
errors = list()
20+
21+
for path in SURVEY_CONFIG_FILE_PATHS:
22+
path = Path(path).absolute()
23+
if not path.exists():
24+
errors.append(
25+
Error(f"File not found: {path}", hint="Make sure the survey config file is present.")
26+
)
27+
28+
return errors

SORT/checks/vite_manifest.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from pathlib import Path
2+
import django.core.checks
3+
import django.conf
4+
import django.contrib.staticfiles.finders
5+
6+
settings = django.conf.settings
7+
8+
9+
@django.core.checks.register(django.core.checks.Tags.staticfiles)
10+
def check_manifest(*args, **kwargs) -> list[django.core.checks.Error]:
11+
"""
12+
Ensure that the Vite manifest file is present.
13+
https://vite.dev/guide/backend-integration
14+
15+
This is a Django system check:
16+
https://docs.djangoproject.com/en/5.2/topics/checks/
17+
"""
18+
errors = list()
19+
20+
# Check the settings is specified
21+
if not getattr(settings, 'VITE_MANIFEST_FILE_PATH', None):
22+
errors.append(
23+
django.core.checks.Error(
24+
f"Setting not defined VITE_MANIFEST_FILE_PATH",
25+
hint="Add VITE_MANIFEST_FILE_PATH to settings.py",
26+
id="SORT.E001",
27+
),
28+
)
29+
30+
# Find the manifest.json file in the static folder
31+
path = django.contrib.staticfiles.finders.find(settings.VITE_MANIFEST_FILE_PATH)
32+
33+
# Check if manifest.json is present
34+
if not path:
35+
absolute_path = Path(settings.STATIC_ROOT).joinpath(settings.VITE_MANIFEST_FILE_PATH).absolute()
36+
errors.append(
37+
django.core.checks.Error(
38+
f"File not found: '{absolute_path}'",
39+
hint="Ensure that the JavaScript project has been built (npm run build)",
40+
id="SORT.E002",
41+
),
42+
)
43+
44+
return errors

SORT/settings.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -213,11 +213,12 @@ def cast_to_boolean(obj: Any) -> bool:
213213
AUTH_USER_MODEL = "home.User" # FA: replace username with email as unique identifiers
214214

215215
# Vite integration
216-
VITE_BASE_URL = "http://localhost:5173" # Url of vite dev server
217-
VITE_STATIC_DIR = (
218-
"ui-components" # Path to vite-generated asset directory in the static folder
219-
)
220-
VITE_MANIFEST_FILE_PATH = os.path.join(VITE_STATIC_DIR, "manifest.json")
216+
if DEBUG:
217+
VITE_BASE_URL = "http://localhost:5173"
218+
"URL of Vite local development server"
219+
VITE_STATIC_DIR = "ui-components"
220+
"Path to vite-generated asset directory in the static folder"
221+
VITE_MANIFEST_FILE_PATH = Path(os.path.join(VITE_STATIC_DIR, "manifest.json"))
221222

222223
X_FRAME_OPTIONS = "SAMEORIGIN"
223224

@@ -281,4 +282,4 @@ def cast_to_boolean(obj: Any) -> bool:
281282
ACCOUNT_SIGNUP_FIELDS = ["email*", "password1*", "password2*"]
282283
ACCOUNT_LOGIN_METHODS = {"email"}
283284
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
284-
ACCOUNT_EMAIL_VERIFICATION="mandatory"
285+
ACCOUNT_EMAIL_VERIFICATION = "mandatory"

docs/testing.md

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,49 @@
11
# SORT testing
22

3+
There are two testing frameworks in place: one for the frontend (JavaScript and Node.js) and another for the backend (Python Django).
4+
5+
# Frontend testing
6+
7+
## Usage
8+
9+
To run the test suite, use the `test` command via the Node package manager (NPM):
10+
11+
```bash
12+
npm test
13+
```
14+
15+
This will execute Vitest. There are also other testing modes. To run the tests whenever the code changes using a file watcher, run:
16+
17+
```bash
18+
npm run test:watch
19+
```
20+
21+
And to get a coverage report:
22+
23+
```bash
24+
npm run test:coverage
25+
```
26+
27+
## Writing tests
28+
29+
The frontend tests are contained in the `ui_components/tests` directory and are organised to match the source code structure. Each test should test an isolated unit of code. Please read the [introduction to writing tests for Svelte](https://svelte.dev/docs/svelte/testing) and the [Svelte testing library](https://testing-library.com/docs/svelte-testing-library/intro).
30+
31+
# Backend testing
32+
333
The test suite uses the Django testing tools. For more information, please read [Testing in Django](https://docs.djangoproject.com/en/5.1/topics/testing/) and
434
the further [Django testing examples](https://django-testing-docs.readthedocs.io/en/latest/index.html).
535

6-
# Installation
36+
## Installation
737

838
To install the necessary packages in your local development environment, use the `requirements-dev.txt` file.
939

1040
```bash
1141
pip install --upgrade --requirement requirements-dev.txt
1242
```
1343

14-
# Usage
44+
## Usage
1545

16-
## Running the test suite
46+
### Running the test suite
1747

1848
Pleaser read the [running tests](https://docs.djangoproject.com/en/5.1/topics/testing/overview/#running-tests) section of the Django documentation.
1949

@@ -22,22 +52,22 @@ python manage.py test home/tests --parallel=auto --failfast
2252
python manage.py test survey/tests --parallel=auto --failfast
2353
```
2454

25-
## Coverage reports
55+
### Coverage reports
2656

2757
At the end of the GitHub Actions testing workflow, a coverage report will be generated using the [Coverage.py](https://coverage.readthedocs.io/) tool.
2858

29-
# Writing tests
59+
## Writing tests
3060

3161
Please read the Django [writing tests section](https://docs.djangoproject.com/en/5.1/topics/testing/overview/#writing-tests) of the Django documentation. There are unit tests in the `./tests` directory of each Django application.
3262

33-
## Tests
63+
### Tests
3464

3565
The tests are defined in each application in the `tests` directory, where each file is a Python script that contains tests for a different aspect of the app. The filenames must start with `test_`.
3666

37-
## Test cases
67+
### Test cases
3868

3969
There are test case classes defined in the [`SORT.test.test_case`](SORT/test/test_case) module that contain useful methods for testing Django views and the application service layer in the SORT code.
4070

41-
## Object factories
71+
### Object factories
4272

4373
There are factory utilities that are used to create mock objects of our Django models for testing in the [`SORT.test.model_factory`](SORT/test/model_factory) module. This uses the [Factory Boy](https://factoryboy.readthedocs.io/en/stable/index.html) library, which [supports the Django ORM](https://factoryboy.readthedocs.io/en/stable/orms.html#module-factory.django).

home/templatetags/vite_integration.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import json
22
import logging
33
from enum import Enum
4+
from pathlib import Path
45
from os import path
56
from typing import LiteralString
67
from urllib.parse import urljoin
@@ -51,11 +52,12 @@ def vite_asset(asset_path: str) -> str:
5152
else:
5253
global VITE_MANIFEST
5354
if VITE_MANIFEST is None:
54-
vite_manifest_path = finders.find(settings.VITE_MANIFEST_FILE_PATH)
55+
vite_manifest_path = Path(finders.find(settings.VITE_MANIFEST_FILE_PATH))
5556
if vite_manifest_path:
56-
with open(vite_manifest_path, "r") as f:
57-
VITE_MANIFEST = json.load(f)
57+
with vite_manifest_path.open() as file:
58+
VITE_MANIFEST = json.load(file)
5859
else:
60+
logger.error("File not found: %s", vite_manifest_path)
5961
raise FileNotFoundError(
6062
f"Vite manifest file ({settings.VITE_MANIFEST_FILE_PATH}) cannot be found."
6163
)
@@ -85,7 +87,7 @@ def vite_asset(asset_path: str) -> str:
8587

8688

8789
def get_asset_import_tag(
88-
asset_static_path: str | LiteralString | bytes, type: AssetType
90+
asset_static_path: str | LiteralString | bytes, type: AssetType
8991
):
9092
asset_static_path_found = finders.find(asset_static_path)
9193
if asset_static_path_found:

0 commit comments

Comments
 (0)