Purpose: A universal playbook for fixing any frontend issue through AI agent + human collaboration. The human prepares the environment and provides the issue. The agent investigates the problem visually (Playwright MCP), implements the fix, verifies it, and iterates until all CI checks pass.
Architecture: Only the Angular frontend runs locally (for hot reload and code changes). The backend services (DSpace REST API, PostgreSQL, Solr) run in Docker containers.
| Property | Value |
|---|---|
| Framework | Angular 15 + Angular Universal (SSR) |
| Language | TypeScript 4.8 |
| Package manager | Yarn 1.x — never use npm |
| Node.js | 18.x (nvm use 18) — Node 20+ breaks eslint-plugin-jsdoc |
| Unit tests | Jasmine / Karma (~5 300 specs) |
| E2E tests | Cypress 13, Chrome headless |
| Main branch | dtq-dev |
| Repo | dataquest-dev/dspace-angular |
- Install Docker Desktop and start it.
- Install nvm (or nvm-windows):
nvm install 18 nvm use 18
- Install Yarn:
npm install -g yarn - Clone the repo:
git clone https://github.com/dataquest-dev/dspace-angular.git cd dspace-angular
-
Start the backend services in Docker (REST API, PostgreSQL, Solr — needed for e2e and live verification):
docker compose -p ci -f docker/docker-compose-ci.yml up -d docker compose -p ci -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
Verify:
curl http://localhost:8080/server/api/core/sitesreturns JSON.The frontend is NOT started here — it runs locally (see §3).
-
Node version:
node --version→ must bev18.x -
Heap size (prevents OOM):
- PowerShell:
$env:NODE_OPTIONS='--max-old-space-size=4096' - Bash:
export NODE_OPTIONS='--max-old-space-size=4096'
- PowerShell:
-
Install dependencies:
yarn install --frozen-lockfile
If Cypress download fails:
CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile -
Create a feature branch:
git checkout dtq-dev && git pull git checkout -b <dspace-customer>/<short-issue-description>
-
Open the project in VS Code with Copilot / agent enabled.
Here is the GitHub issue to fix: <PASTE ISSUE URL>
Read the agent guide at docs/agents.md first.
Environment info:
- Backend services (REST API, Solr, PostgreSQL) are running in Docker on default ports
- Frontend will run locally (you start it with yarn start:dev or yarn serve:ssr)
- Admin credentials if necessary for the issue: <PASTE EMAIL>/<PASTE PASSWORD>
Please:
1. Read the issue and understand the problem
2. Use Playwright MCP to navigate to the affected page and visually confirm the bug
3. Investigate the codebase to find the root cause
4. Implement a fix
5. Use Playwright MCP again to verify the fix is working
6. Run all CI checks (lint → circ-deps → build → unit tests) and iterate until all pass
7. Only commit and push when everything is green
If the issue involves a specific page, here is the direct URL: <OPTIONAL URL>
That's it. Everything below is the agent's responsibility.
1. READ the issue — understand what's broken and where
2. DETECT with Playwright MCP — navigate to the affected page, take snapshots,
confirm the bug visually (duplicate IDs, broken layout, wrong behavior, etc.)
3. SEARCH the codebase — find the root cause in the source files
4. IMPLEMENT the fix — minimal, focused changes
5. VERIFY with Playwright MCP — navigate to the same page, confirm the fix works
6. RUN CI checks in order — fix any failures, re-verify after each code change
7. COMMIT & PUSH — only when everything passes
Every step must pass before proceeding to the next.
| # | Step | Command | Time | Pass Criteria |
|---|---|---|---|---|
| 1 | Lint | yarn run lint --quiet |
~76s | Exit 0, "All files pass linting." |
| 2 | Circular deps | yarn run check-circ-deps |
~34s | "No circular dependency found!" |
| 3 | Build | yarn run build:prod |
~6.5min | Two bundles (browser + server), no Error: lines |
| 4 | Unit tests | yarn run test:headless |
~3.5min | ~5300 specs, 0 failures |
| 5 | E2E tests | See §3.5 | ~5min | No new failures vs. dtq-dev baseline |
If any step fails: fix the code → rerun that same step → only proceed when green.
yarn run test:headless --include='**/path/to/component.spec.ts'The check-circ-deps script uses madge --exclude with regex containing |. PowerShell interprets | as a pipeline operator. Use the stop-parsing token:
npx --% madge --exclude "(bitstream|bundle|collection|config-submission-form|eperson|item|version)\.model\.ts$" --circular --extensions ts ./Start the SSR server:
yarn run build:prod
yarn run serve:ssr &
# Wait for http://localhost:4000 to respondRun public-page specs (always safe, no login needed):
npx cypress run --spec "cypress/e2e/footer.cy.ts,cypress/e2e/header.cy.ts,cypress/e2e/pagenotfound.cy.ts,cypress/e2e/browse-by-title.cy.ts,cypress/e2e/browse-by-author.cy.ts,cypress/e2e/browse-by-subject.cy.ts,cypress/e2e/community-list.cy.ts,cypress/e2e/search-page.cy.ts" --browser chromeOn PowerShell, set the base URL first:
$env:CYPRESS_BASE_URL="http://localhost:4000"| Category | Spec files | Requires |
|---|---|---|
| Public pages (always safe) | footer, header, pagenotfound, browse-by-*, community-list, search-page, feedback |
Frontend only |
| Data-dependent | collection-page, community-page, item-page |
Backend + Demo Entities assetstore |
| Login-required | submission*, admin-*, my-dspace, profile-page, handle-page, health-page |
Backend + Demo Entities + valid credentials |
git add <changed-files>
# NEVER commit: config/config.yml, .env.* files, coverage/, cypress/videos/
git commit -m "fix: <concise description>"
git push origin ufal/<branch-name>Playwright MCP is the agent's eyes. Use it to see the problem and confirm the fix.
browser_navigate → http://localhost:4000/path/to/page
browser_snapshot → get the accessibility tree of the page
Use browser_evaluate to inspect the DOM. Examples:
Check for duplicate HTML IDs:
async (page) => {
return await page.evaluate(() => {
const ids = [...document.querySelectorAll('[id]')].map(el => el.id).filter(Boolean);
const counts = {};
ids.forEach(id => { counts[id] = (counts[id] || 0) + 1; });
const dupes = Object.entries(counts).filter(([_, c]) => c > 1);
return { total: ids.length, unique: new Set(ids).size, duplicates: dupes };
});
}Verify specific selectors still work (backward compatibility):
async (page) => {
return await page.evaluate(() => {
const selectors = ['input#dc_title', 'label[for=dc_title]', '.some-class'];
return selectors.map(s => `${s}: ${document.querySelectorAll(s).length}`);
});
}Check console errors:
async (page) => {
const errors = [];
page.on('console', msg => { if (msg.type() === 'error') errors.push(msg.text()); });
await page.reload();
await page.waitForTimeout(3000);
return errors;
}Some pages require authentication (submission forms, admin panels).
Test credentials are defined in cypress.config.ts (look for DSPACE_TEST_ADMIN_USER / DSPACE_TEST_ADMIN_PASSWORD).
- Navigate to the login page
- Dismiss the DiscoJuice/Shibboleth overlay if it appears:
await page.evaluate(() => { document.querySelectorAll('[class*="discojuice"]').forEach(el => el.remove()); });
- Fill in credentials and submit the login form
- Navigate to the target page
Use browser_take_screenshot before and after the fix to document the change.
| Mode | Command | CORS | Hot Reload |
|---|---|---|---|
| Dev server | yarn start:dev |
dspace.ui.url port |
✅ Yes |
| SSR mode | yarn build:prod → yarn serve:ssr |
✅ No issues (server-side proxy) | ❌ No |
Prefer SSR mode for Playwright verification. It avoids CORS issues and reflects the real production behavior. Use dev server only when you need rapid iteration.
Before panicking about a red test, check if it was already failing on dtq-dev:
| Symptom | Affected Specs | Cause |
|---|---|---|
DiscoJuice display: none click fails |
Login-dependent tests | Docker env popup issue |
cy.wait('@viewevent') timeout |
collection-page.cy.ts |
Matomo not configured |
| Entity redirect fails | item-page.cy.ts |
Backend routing issue |
link-in-text-block a11y violation |
privacy.cy.ts, end-user-agreement.cy.ts |
CSS styling issue |
Rule: Only fix failures that YOUR changes caused. If a test was already failing on dtq-dev, leave it alone.
These come from real agent sessions. Read them before writing code.
<!-- ❌ WRONG — interpolation in non-standard attributes -->
<div aria-labelledby="prefix-{{ var }}">
<!-- ✅ CORRECT — property binding -->
<div [attr.aria-labelledby]="'prefix-' + var">This applies to all aria-* and custom HTML attributes. Always use [attr.X]="expression" instead of X="{{ interpolation }}".
HTML id attributes must not contain whitespace or special characters. Sanitize dynamic data:
sanitizedName = rawName.replace(/\s+/g, '');If a description element is conditionally rendered (*ngIf), the aria-describedby pointing to it must also be conditional:
<span *ngIf="label" [id]="'desc-' + uniqueId">{{ label }}</span>
<input [attr.aria-describedby]="label ? 'desc-' + uniqueId : null">Submission forms use IDs like input#dc_title, label[for=local_hasCMDI] etc. These are referenced by Cypress e2e selectors and formModel definitions. Changing them cascades into dozens of broken tests. Unless the issue specifically requires it, leave form IDs alone.
Many child form components reference 'label_' + model.id in [attr.aria-labelledby]. If you change a label's [id], you break accessibility wiring in 13+ template files. Keep label IDs stable — if deduplicating, change the form control ID, not the label.
This project uses { teardown: { destroyAfterEach: false } } (see src/test.ts), so ngOnDestroy does NOT run between unit tests. Any static registry or singleton must be explicitly cleared in the global afterEach in src/test.ts, or it will leak state across specs.
If your change causes dozens of cascading test failures, stop and rethink. It's cheaper to revert the whole approach than to chase 50 broken selectors. A minimal, targeted fix is always better than a sweeping refactor.
| Issue | Solution |
|---|---|
check-circ-deps fails with pipe error |
Use npx --% stop-parsing token |
&& not valid in PowerShell 5.1 |
Use ; to chain commands |
| Env vars don't persist | Use $env:VAR='val' before each command |
These are pre-existing and harmless:
Warning: X.component.ts is unused(theme components)Warning: CommonJS or AMD dependenciesWarning: bundle exceeded maximum budget
Real errors always start with Error:.
All code must work with Angular Universal. Never use document or window directly:
import { isPlatformBrowser } from '@angular/common';
if (isPlatformBrowser(this.platformId)) {
// browser-only code
}When changing an id, class, or tag in a template, check the corresponding .spec.ts:
grep -n "By.css" src/app/.../my-component.component.spec.tsIf a test uses a selector you changed, update it or the test will fail.
This section documents a real fix for reference. The patterns here apply to similar problems.
Problem: The submission edit page renders dynamic form fields via @ng-dynamic-forms. When the same metadata field appears in multiple DynamicRowGroupModel groups, each instance gets the same HTML ID, producing duplicates.
Root cause: @ng-dynamic-forms/core getElementId(model) only handles DynamicFormArrayGroupModel parents. It does NOT handle DynamicFormGroupModel / DynamicRowGroupModel parents, even though they have unique auto-generated IDs.
Solution: A UniqueIdRegistry static class that:
- First occurrence of a base ID → returns the original ID (preserves Cypress selectors)
- Subsequent occurrences → appends
_1,_2, etc. via a monotonic counter - Components call
register()on init andrelease()on destroy
The get id() override was added to DsDynamicFormControlContainerComponent and DsDynamicScrollableDropdownComponent.
Also fixed: id="license_option_{{ license.id }}" → [id]="'license_option_' + license.id" (interpolation rendered empty because Angular consumed it before DOM render).
Docker runs only the backend services. The Angular frontend always runs locally.
| File | Purpose |
|---|---|
docker-compose-ci.yml |
Backend stack for CI/testing (DSpace REST + PostgreSQL + Solr) |
docker-compose.yml |
Full stack dev (includes a frontend container — but we run frontend locally instead) |
docker-compose-rest.yml |
REST backend only |
cli.yml + cli.assetstore.yml |
Load Demo Entities test data |
| Service | Port | Where |
|---|---|---|
| DSpace REST API | 8080 | Docker |
| PostgreSQL | 5432 | Docker |
| Solr | 8983 | Docker |
| Angular Frontend | 4000 | Local (not Docker) |
Test credentials and UUIDs for e2e tests are defined in cypress.config.ts under the env section. Look for DSPACE_TEST_ADMIN_USER, DSPACE_TEST_ADMIN_PASSWORD, DSPACE_TEST_COMMUNITY, etc. These are standard DSpace demo-instance values — do not replace them with real credentials.
The backend's dspace.ui.url determines CORS allowed origins. Your local dev server port must match this URL, or browser XHR requests will be blocked.
- Check which UI port the backend expects (look at the Docker compose env vars for
dspace.ui.url) - Edit
config/config.ymltemporarily to match those ports - Run:
yarn start:dev - Never commit
config/config.ymlchanges — add it to your local.gitignoreor alwaysgit checkoutbefore committing
Alternatively, use SSR mode (yarn build:prod → yarn serve:ssr) which proxies API calls server-side and avoids CORS entirely. This is the recommended approach for Playwright verification.
# Start
docker compose -p ci -f docker/docker-compose-ci.yml up -d
docker compose -p ci -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
# Verify
curl http://localhost:8080/server/api/core/sites
# Stop
docker compose -p ci -f docker/docker-compose-ci.yml down| Error | Fix |
|---|---|
The engine "node" is incompatible |
nvm use 18 |
Cypress App could not be downloaded |
CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile |
| Out of memory during build | $env:NODE_OPTIONS='--max-old-space-size=4096' |
yarn.lock merge conflicts |
git checkout --theirs yarn.lock; yarn install; git add yarn.lock |
Can't bind to 'aria-labelledby' |
Use [attr.aria-labelledby]="expr" not interpolation |
| Cypress can't connect | Run yarn serve:ssr first; check CYPRESS_BASE_URL |
| CORS errors with dev server | Match port to backend's dspace.ui.url, or use SSR mode |
madge pipe error on PowerShell |
Use npx --% stop-parsing token |
Test fails with Cannot find element |
You renamed an ID — update the test selector too |
| DiscoJuice overlay blocks login | document.querySelector('.discojuice-overlay')?.remove() |
| Convention | Rule |
|---|---|
| Component prefix | ds- |
| Strings | Single quotes |
| Indentation | 2 spaces |
| Imports | Specific files, not barrels |
| Lodash | import get from 'lodash/get' |
| SSR | No raw document/window — use isPlatformBrowser() |
| Tests | Co-located .spec.ts files |
| JSDoc | Required on all new public methods |
# Setup
nvm use 18
yarn install --frozen-lockfile
# CI validation (run in order)
yarn run lint --quiet
yarn run check-circ-deps
yarn run build:prod
yarn run test:headless
# Single test
yarn run test:headless --include='**/path/to/test.spec.ts'
# Dev server
yarn run start:dev
# E2E
yarn run build:prod
yarn run serve:ssr &
npx cypress run --spec "cypress/e2e/footer.cy.ts" --browser chrome
# Docker backend
docker compose -p ci -f docker/docker-compose-ci.yml up -d
docker compose -p ci -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
docker compose -p ci -f docker/docker-compose-ci.yml down