Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
.DS_Store
build/
dist/
coverage/
bin/
node_modules/
vendor/
Expand All @@ -13,6 +14,7 @@ vendor/
tmp.scss
.tscache
a.env
.env
dist/
.angulardoc.json
.nx
Expand Down
4 changes: 0 additions & 4 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,5 @@ e2e
CHANGELOG.md
JPlag-Report-Viewer/
.github/
karma/
Gruntfile.js
karma.conf.js
polyfills.ts
test.ts
main.ts
5 changes: 3 additions & 2 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,15 +79,16 @@
}
],
"optimization": true,
"outputHashing": "bundles",
"outputHashing": "all",
"sourceMap": false,
"extractLicenses": true,
"serviceWorker": "ngsw-config.json"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
"sourceMap": true,
"serviceWorker": "ngsw-config.json"
},
"testing": {
"aot": false,
Expand Down
130 changes: 130 additions & 0 deletions docs/ISSUE-1290-testing-strategy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# Vitest coverage and next integration checks (#1290)

This records the September 2026 test audit for
[doubtfire-web #1290](https://github.com/doubtfire-lms/doubtfire-web/issues/1290).
The application uses Angular's unit-test builder with Vitest, a jsdom environment,
and the setup file in `src/vitest-setup.ts`. The normal pull request suite does not
require a running API, browser installation, production account, or secrets.

## Running the checks

Use the Node version in `.nvmrc` (Node 22.22.3 or later in the supported Node 22
line), then run:

```sh
npm ci
npm run test:ci
npm run build
```

For a focused iteration, keep Angular's compilation step and select the spec:

```sh
npm run test:ci -- --include='src/app/common/file-downloader/file-downloader.service.spec.ts'
```

Run the whole suite before opening a PR. A focused run cannot detect interactions
with module registration, template compilation, or the rest of the application's
shared fixtures. Existing GitHub `Test CI` and `Node.js CI` workflows run the full
unit suite and application build on pull requests.

## Recorded validation

On the FL-22 / #1290 branch based on `11.0.x` commit `a35f2826d`, a clean
`npm ci`, `npm run build`, and the full `npm run test:ci` passed with Node 22.23.2.
The suite contains 146 files and 1107 passing tests. Targeted ESLint also passed.
The runner reports an existing nested `vi.mock` warning in the PDF viewer spec;
it is not a test failure. These results cover this branch, not future merges or
the longer-term browser/API jobs below.

## Gaps addressed

The audit found 143 existing spec files under `src/app`. The new tests focus on
observable behaviour in shared code that previously had no dedicated tests:

| Area | Regression checks | Defect caught |
| -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| File downloader | Complete and multipart responses, requested byte bounds, missing/malformed ranges, gaps/duplicates, changing totals, interrupted transfers, and a server ignoring Range | Invalid ranges could leave the caller waiting or publish corrupt data; continuation end indexes were one too large; a full response was appended to already received bytes |
| Authentication interceptor | API and LTI credentials, anonymous requests, external/static requests, preservation of caller headers and immutable requests | These cases now protect the shared request boundary |
| Authentication return URL | Blocked browser storage, malformed persisted entries, future timestamps, exact expiry boundary, and one-use consumption | Extends existing unsafe URL, SSO, logout, and expiry checks |

The downloader tests use `HttpTestingController` to assert the real `HttpClient`
request and response contract, without reaching an actual server. They verify the
assembled Blob contents rather than only checking that a callback ran. Every
case verifies no unmatched requests remain. The auth tests use harmless fixture
tokens and exercise the interceptor/service's public methods.

## Remaining coverage to prioritise

This is a risk-based audit, not a claim of complete statement or branch coverage.
Add tests with each change rather than treating creation-only specs as evidence
that every interaction works.

1. **Authentication refresh concurrency:** several simultaneous expired requests,
failed refresh releasing every waiting subscriber, retry headers, and logout
while a refresh is in flight. `HttpErrorInterceptor` currently has error-body
extraction coverage but needs these stateful scenarios.
2. **Submission state transitions:** combine the upload modal, processing/retry API
response, and selected task route to show that failed uploads retain the user's
context and a successful upload returns to the intended dashboard.
3. **Staff workflows:** task-sheet navigation, tutorial changes, bulk portfolio
operations, and permission refusals. Prefer DOM events and rendered state over
direct calls to private component methods.
4. **Real browser behaviours:** focus restoration, drag/drop, recorded audio,
actual CSS breakpoints, Blob downloads, and service workers. jsdom does not
establish visual layout, accessibility, or browser permission behaviour.
5. **API compatibility:** serialization of deadlines, extensions and upload errors
across the web/API boundary. A frontend mock cannot prove the server emits the
expected data or enforces permissions.

## Longer-term API test approach

Keep deterministic frontend contract tests in the normal Vitest suite using
[`provideHttpClientTesting` and `HttpTestingController`](https://angular.dev/guide/http/testing).
Assert request methods, authorization conditions, query/body formats, and error
responses; do not simply return whatever object the component expects. Keep
fixture payloads near the consuming feature and link the corresponding API
endpoint/regression test in its PR.

Add a separate integration CI job for contracts that require the real API. Boot
pinned web/API/deploy revisions against an isolated disposable database, run
migrations and a small deterministic seed, wait for readiness, then authenticate
through the same endpoints used by the client. Use two fixture students and a
staff member in distinct units to test both allowed requests and cross-user
refusals. Run API model/request tests in the API repository as well; frontend
integration checks supplement them. Tear down the database after each job and
avoid production credentials or public notification delivery.

Start with three contracts: loading a student's enrolled units; reading the same
task's actual effective deadline; submitting a small fixture file and polling its
processing status. Add an explicit assertion for each relevant 4xx error. This
job should have its own failure report so an API startup failure is not presented
as a frontend assertion failure.

## Longer-term headless browser approach

Use two separate layers with a documented purpose:

- **Browser component tests:** Angular supports
[Vitest browser mode](https://angular.dev/guide/testing) for interactions requiring
a real DOM/browser. Pilot focus and download behaviours before moving large
numbers of otherwise fast jsdom tests.
- **Student journey tests:** a Playwright suite against the built application.
Its [`webServer` configuration](https://playwright.dev/docs/test-webserver) can
manage an isolated local server. For fast PR checks,
[route mocks](https://playwright.dev/docs/network) should cover login, loading a
unit, selecting a task, submitting a file, and the resulting dashboard state.
Add failures for rejected credentials, unavailable units, and failed uploads.
Start with Chromium, then add Firefox/WebKit for browser-specific controls.

The real API job should reuse the same journeys without route mocks, against the
seeded stack. Keep mocked and real API runs explicitly named; passing a mocked
journey does not prove server compatibility. Use role/name locators and web-first
assertions rather than fixed sleeps. Store a trace/screenshot only on failure,
redact test authorization headers, and keep fixture users' data synthetic. A
service-worker-specific test must run separately from tests that block service
workers for reliable network interception.

These longer-term jobs are an investigated implementation plan, not new CI
requirements in this PR. They need their own pinned runtime fixtures and review
before becoming required checks.
68 changes: 68 additions & 0 deletions docs/cross-project-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Cross-project UI handover

Tickets: CPD-FB02, CPD-FB03, CPD-UI03–06, PR-XU-12, PR-XU-13 and PR-XU-20.
The API's projects logging and access-control tests are maintained separately.
Calendar features are unchanged.

## Feedback and filtering

`has_feedback` maps to `Task.hasFeedback`. `true` means staff feedback exists;
`false` means none exists; missing metadata stays unavailable. No feedback body,
marker note or additional student identity is fetched for a card. The unread
comment count is separate: it cannot establish whether staff feedback is new.
The UI therefore labels it **unread comments**, never **new feedback**.

The Staff feedback selector combines with status, grade, date range and the
existing global/per-unit searches. Search includes the displayed phrases
`staff feedback available`, `no staff feedback`, `feedback unavailable`, and
`unread comments` where applicable. Searching just `feedback` can match any of
these states; use the selector to ask specifically for available feedback.
Clear all restores all feedback states while preserving per-unit controls.

The Staff feedback action navigates to
`/projects/:projectId/dashboard/:abbreviation/feedback`, which opens the existing
feedback pane on narrow screens even after comments have been read. The normal
task title links to task details. Neither route bypasses API authorisation.

## Layout, controls and recovery

The grid wraps cards in DOM order using a minimum bounded by the available width.
Project name, task completion count, deadline warning and project link remain
available in phone summaries. Comfortable/compact density changes spacing,
not data. Long names wrap. Task controls and focus outlines retain their size.

The local `panelState` template accepts `title`, `message` and optional `busy`.
It gives each panel a named section, minimum height and polite status text.
Use plain messages without response bodies or stack traces. Active units,
previous units and recommended order have separate busy/error state. Failed
refreshes retain the last successful data; initial empty results get explicit
messages. Refresh buttons remain mounted and keyboard-focusable while busy,
use `aria-disabled`, and guard repeat activation in their handlers.

Active projects initially come from the application's global loading contract.
The dashboard does not replace the app's sign-in/bootstrap error handling.
Previous projects use a separate cache so loading history cannot contaminate
the active project cache. All component-owned requests stop on destruction.

Filter/sort menus use native buttons with checked states. The result count is
announced in one polite live region; input focus remains on the control that
changed the results. Phone disclosures retain their toggle and `aria-expanded`.
Count badges use the existing `#3939ff` token with white text (6.56:1 contrast),
and explicitly name the count and task. Unread comments are not exposed as an
unexplained digit.

## Validation

Use synthetic projects for browser checks at 320, 390, 768, 1024 and 1440 px,
in both density modes, with long names and enough tasks to scroll. Exercise
available/no/unavailable feedback, no matches, empty units, previous-unit
failure/retry, and a failed refresh with existing data. Tab through every
control; open filters with the keyboard; verify focus survives refresh. At
200% zoom check wrapping and the continued availability of the project link.

The focused component tests cover combined filters, missing metadata, actual
task/feedback hrefs, stale data and recovery, duplicate refresh suppression,
focus retention, counts and density. Route tests cover student access and
scope restoration. Browser, test and build results for this change are recorded
in the [validation record](evidence/cross-project-ui/README.md) and PR. Real screen-reader sign-off remains
a reviewer activity; automated accessible-name assertions do not claim it.
36 changes: 36 additions & 0 deletions docs/cross-unit-dashboard.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Cross-project dashboard

The student-only `/dashboard` route shows authorised tasks across active and
previous units. It supports combined unit/task search, status, grade, date and
staff-feedback filters, task ordering, and direct project/task/feedback links.

The dashboard was ported from the old 10.x cross-unit branch onto 11.0.x and
translated from UI-Router to Angular Router. It is now implemented and tested
on `11.0.x`; the original root-level skeleton status is retired.

## Sources and verification

- `src/app/dashboard/f-cross-dashboard.component.{ts,html,scss}` owns mapping,
filters, grid, density and panel states.
- `src/app/dashboard/list-item/` owns task rows and expanded actions.
- `src/app/app.routes.ts` owns student route access and feedback destinations.
- `src/app/projects/states/index/global-state.service.ts` loads active projects;
`ProjectService` requests previous units with task definitions in a separate cache.
- The API's `GET /projects` scopes project/task metadata to the authenticated user.

Run `npm run test:ci -- --include='src/app/dashboard/**/*.spec.ts'` using the
repository's Angular Vitest builder. See [UI handover](cross-project-ui.md) for
current validation results, state behaviour and feedback semantics. Review
[privacy-safe evidence guidance](CPD-Q06-privacy-safe-screenshot-checklist.md)
before capturing examples.

## Sample data

These values are entirely synthetic and are intended only as a visual reference for dashboard
mockups. They are not an API schema or an automated-test fixture. Dates use `DD/MM/YYYY`.

| Unit code | Example task due date | Unit completion | Example task status |
| --------- | --------------------- | --------------- | ------------------- |
| COS10001 | 17/10/2026 | 10% | `not_started` |
| COS20007 | 19/08/2026 | 100% | `complete` |
| COS30046 | 25/08/2026 | 90% | `working_on_it` |
12 changes: 8 additions & 4 deletions src/app/account/edit-profile/edit-profile.component.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<div FxLayout="row" fxLayoutAlign="center center" id="parent">
<div fxFlex.gt-sm="20"></div>
<div class="form-container" fxFlex>
<div class="flex flex-row items-center justify-center" id="parent">
<div
class="min-[960px]:max-w-[20%] min-[960px]:shrink min-[960px]:grow min-[960px]:basis-full"
></div>
<div class="form-container flex-1">
@if (!loading) {
<f-edit-profile-form mode="edit"></f-edit-profile-form>
}
</div>
<div fxFlex.gt-sm="20"></div>
<div
class="min-[960px]:max-w-[20%] min-[960px]:shrink min-[960px]:grow min-[960px]:basis-full"
></div>
</div>
8 changes: 7 additions & 1 deletion src/app/account/edit-profile/edit-profile.component.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
#parent {
margin-top: 30px;
margin: 0 auto;
min-width: 0;
padding-block: clamp(12px, 3vw, 30px);
width: 100%;
}

.form-container {
min-width: 0;
width: 100%;
max-width: 800px;
padding-inline: 1rem;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,18 @@ <h1 class="mb-0.5">Activities</h1>
</div>
@if (loadingActivities) {
<div aria-busy="true" aria-label="Loading activities" class="overflow-hidden">
<div class="flex h-14 items-center gap-6 border-b border-gray-300 px-4">
@for (width of ['30%', '22%', '4%']; track $index) {
<ngx-skeleton-loader
[theme]="{width: width, height: '12px', 'border-radius': '999px', margin: '0'}"
></ngx-skeleton-loader>
}
</div>

@for (row of skeletonRows; track row) {
<div class="flex h-[72px] items-center gap-6 border-b border-y-gray-300 px-4">
<ngx-skeleton-loader
class="w-[50%]"
[theme]="{width: '35%', height: '12px', 'border-radius': '999px', margin: '0'}"
></ngx-skeleton-loader>
<ngx-skeleton-loader
class="w-[50%]"
[theme]="{width: '35%', height: '12px', 'border-radius': '999px', margin: '0'}"
></ngx-skeleton-loader>
<ngx-skeleton-loader
appearance="circle"
[theme]="{width: '28px', height: '28px', margin: '0'}"
></ngx-skeleton-loader>
</div>
}
<f-skeleton-loader
rowHeight="56px"
shape="table-row"
[columns]="3"
[count]="1"
></f-skeleton-loader>
<f-skeleton-loader
rowHeight="72px"
shape="table-row"
[columns]="3"
[count]="3"
></f-skeleton-loader>
</div>
}

Expand Down Expand Up @@ -125,7 +113,7 @@ <h1 class="mb-0.5">Activities</h1>
</td>
<td *matFooterCellDef mat-footer-cell>
<div class="edit-actions right">
<button mat-icon-button (click)="submit()">
<button aria-label="Add activity type" mat-icon-button (click)="submit()">
<mat-icon>add</mat-icon>
</button>
</div>
Expand All @@ -134,6 +122,15 @@ <h1 class="mb-0.5">Activities</h1>

<tr *matHeaderRowDef="columns" mat-header-row></tr>
<tr *matRowDef="let row; columns: columns" mat-row></tr>
<tr *matNoDataRow class="mat-row">
<td class="mat-cell !border-b-0" [attr.colspan]="columns.length">
<f-empty-state
hint="Add an activity using the row below."
icon="assignment"
message="No activities yet"
></f-empty-state>
</td>
</tr>
<tr *matFooterRowDef="columns" mat-footer-row [hidden]="selected"></tr>
</table>
</div>
Loading