diff --git a/.browserslistrc b/.browserslistrc deleted file mode 100644 index f8a421c3304..00000000000 --- a/.browserslistrc +++ /dev/null @@ -1,11 +0,0 @@ -# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. -# For additional information regarding the format and rule options, please see: -# https://github.com/browserslist/browserslist#queries - -# You can see what browsers were selected by your queries by running: -# npx browserslist - -> 0.5% -last 2 versions -Firefox ESR -not IE 9-11 # For IE 9-11 support, remove 'not'. diff --git a/.editorconfig b/.editorconfig index 15d4c87b142..590d1dea081 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,3 +15,6 @@ trim_trailing_whitespace = false [*.ts] quote_type = single + +[*.json5] +ij_json_keep_blank_lines_in_code = 3 diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000000..6920cc47121 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,269 @@ +{ + "root": true, + "plugins": [ + "@typescript-eslint", + "@angular-eslint/eslint-plugin", + "eslint-plugin-import", + "eslint-plugin-jsdoc", + "eslint-plugin-deprecation", + "unused-imports", + "eslint-plugin-lodash", + "eslint-plugin-jsonc" + ], + "overrides": [ + { + "files": [ + "*.ts" + ], + "parserOptions": { + "project": [ + "./tsconfig.json", + "./cypress/tsconfig.json" + ], + "createDefaultProgram": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + "plugin:@angular-eslint/recommended", + "plugin:@angular-eslint/template/process-inline-templates" + ], + "rules": { + "max-classes-per-file": [ + "error", + 1 + ], + "comma-dangle": [ + "off", + "always-multiline" + ], + "eol-last": [ + "error", + "always" + ], + "no-console": [ + "error", + { + "allow": [ + "log", + "warn", + "dir", + "timeLog", + "assert", + "clear", + "count", + "countReset", + "group", + "groupEnd", + "table", + "debug", + "info", + "dirxml", + "error", + "groupCollapsed", + "Console", + "profile", + "profileEnd", + "timeStamp", + "context" + ] + } + ], + "curly": "error", + "brace-style": [ + "error", + "1tbs", + { + "allowSingleLine": true + } + ], + "eqeqeq": [ + "error", + "always", + { + "null": "ignore" + } + ], + "radix": "error", + "guard-for-in": "error", + "no-bitwise": "error", + "no-restricted-imports": "error", + "no-caller": "error", + "no-debugger": "error", + "no-redeclare": "error", + "no-eval": "error", + "no-fallthrough": "error", + "no-trailing-spaces": "error", + "space-infix-ops": "error", + "keyword-spacing": "error", + "no-var": "error", + "no-unused-expressions": [ + "error", + { + "allowTernary": true + } + ], + "prefer-const": "off", // todo: re-enable & fix errors (more strict than it used to be in TSLint) + "prefer-spread": "off", + "no-underscore-dangle": "off", + + // todo: disabled rules from eslint:recommended, consider re-enabling & fixing + "no-prototype-builtins": "off", + "no-useless-escape": "off", + "no-case-declarations": "off", + "no-extra-boolean-cast": "off", + + "@angular-eslint/directive-selector": [ + "error", + { + "type": "attribute", + "prefix": "ds", + "style": "camelCase" + } + ], + "@angular-eslint/component-selector": [ + "error", + { + "type": "element", + "prefix": "ds", + "style": "kebab-case" + } + ], + "@angular-eslint/pipe-prefix": [ + "error", + { + "prefixes": [ + "ds" + ] + } + ], + "@angular-eslint/no-attribute-decorator": "error", + "@angular-eslint/no-forward-ref": "error", + "@angular-eslint/no-output-native": "warn", + "@angular-eslint/no-output-on-prefix": "warn", + "@angular-eslint/no-conflicting-lifecycle": "warn", + + "@typescript-eslint/no-inferrable-types":[ + "error", + { + "ignoreParameters": true + } + ], + "@typescript-eslint/quotes": [ + "error", + "single", + { + "avoidEscape": true, + "allowTemplateLiterals": true + } + ], + "@typescript-eslint/semi": "error", + "@typescript-eslint/no-shadow": "error", + "@typescript-eslint/dot-notation": "error", + "@typescript-eslint/consistent-type-definitions": "error", + "@typescript-eslint/prefer-function-type": "error", + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": "property", + "format": null + } + ], + "@typescript-eslint/member-ordering": [ + "error", + { + "default": [ + "static-field", + "instance-field", + "static-method", + "instance-method" + ] + } + ], + "@typescript-eslint/type-annotation-spacing": "error", + "@typescript-eslint/unified-signatures": "error", + "@typescript-eslint/ban-types": "warn", // todo: deal with {} type issues & re-enable + "@typescript-eslint/no-floating-promises": "warn", + "@typescript-eslint/no-misused-promises": "warn", + "@typescript-eslint/restrict-plus-operands": "warn", + "@typescript-eslint/unbound-method": "off", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-var-requires": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/no-unnecessary-type-assertion": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-unsafe-assignment": "off", + "@typescript-eslint/no-unsafe-member-access": "off", + "@typescript-eslint/no-unsafe-call": "off", + "@typescript-eslint/no-unsafe-argument": "off", + "@typescript-eslint/no-unsafe-return": "off", + "@typescript-eslint/restrict-template-expressions": "off", + "@typescript-eslint/require-await": "off", + + "deprecation/deprecation": "warn", + + "import/order": "off", + "import/no-deprecated": "warn", + "import/no-namespace": "error", + "unused-imports/no-unused-imports": "error", + "lodash/import-scope": [ + "error", + "method" + ] + } + }, + { + "files": [ + "*.html" + ], + "extends": [ + "plugin:@angular-eslint/template/recommended" + ], + "rules": { + // todo: re-enable & fix errors + "@angular-eslint/template/no-negated-async": "off", + "@angular-eslint/template/eqeqeq": "off" + } + }, + { + "files": [ + "*.json5" + ], + "extends": [ + "plugin:jsonc/recommended-with-json5" + ], + "rules": { + // The ESLint core no-irregular-whitespace rule doesn't work well in JSON + // See: https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-irregular-whitespace.html + "no-irregular-whitespace": "off", + "jsonc/no-irregular-whitespace": "error", + "no-trailing-spaces": "error", + "jsonc/comma-dangle": [ + "error", + "always-multiline" + ], + "jsonc/indent": [ + "error", + 2 + ], + "jsonc/key-spacing": [ + "error", + { + "beforeColon": false, + "afterColon": true, + "mode": "strict" + } + ], + "jsonc/no-dupe-keys": "off", + "jsonc/quotes": [ + "error", + "double", + { + "avoidEscape": false + } + ] + } + } + ] +} diff --git a/.gitattributes b/.gitattributes index dfe0770424b..f5e28069871 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,16 @@ -# Auto detect text files and perform LF normalization +# By default, auto detect text files and perform LF normalization +# This ensures code is always checked in with LF line endings * text=auto + +# JS and TS files must always use LF for Angular tools to work +# Some Angular tools expect LF line endings, even on Windows. +# This ensures Windows always checks out these files with LF line endings +# We've copied many of these rules from https://github.com/angular/angular-cli/ +*.js eol=lf +*.ts eol=lf +*.json eol=lf +*.json5 eol=lf +*.css eol=lf +*.scss eol=lf +*.html eol=lf +*.svg eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 8e4ed0811d5..00000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug, needs triage -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. Include the version(s) of DSpace where you've seen this problem & what *web browser* you were using. Link to examples if they are public. - -**To Reproduce** -Steps to reproduce the behavior: -1. Do this -2. Then this... - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Related work** -Link to any related tickets or PRs here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 34cc2c9e4f3..00000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest a new feature for this project -title: '' -labels: new feature, needs triage -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives or workarounds you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/actions/erase-db/action.yml b/.github/actions/erase-db/action.yml new file mode 100644 index 00000000000..9ec7e828cd1 --- /dev/null +++ b/.github/actions/erase-db/action.yml @@ -0,0 +1,36 @@ +name: 'Erase dspace db' +description: 'CI/CD Erase db' + +inputs: + INSTANCE: + description: 'port suffix' + required: true + type: string + NAME: + description: 'docker compose project name' + required: true + type: string + +runs: + using: "composite" + steps: + + - name: stop and remove containers + shell: bash + env: + INSTANCE: ${{ inputs.INSTANCE }} + run: | + docker stop dspacesolr$INSTANCE dspacedb$INSTANCE dspace$INSTANCE dspace-angular$INSTANCE || true + docker rm dspacesolr$INSTANCE dspacedb$INSTANCE dspace$INSTANCE dspace-angular$INSTANCE || true + + - name: remove volumes + shell: bash + env: + NAME: ${{ inputs.NAME }} + run: | + # # condition below was found by accident and appears to be useless. Investigate later. + # be sure to have INSTANCE set + # if [[ "x${NAME}" != "dspace-" ]]; then + docker volume rm $(docker volume ls --filter name="${NAME}_" -q) || true + # fi; + diff --git a/.github/actions/import-db/action.yml b/.github/actions/import-db/action.yml new file mode 100644 index 00000000000..15c3b355089 --- /dev/null +++ b/.github/actions/import-db/action.yml @@ -0,0 +1,105 @@ +name: 'Import dspace db' +description: 'CI/CD import db' + +inputs: + DATADIR: + description: 'data dir with dump, icons' + required: true + type: string + INSTANCE: + description: 'port suffix' + required: true + type: string + ASSETSTORE: + description: 'location of assetstore folder' + required: false + type: string + ADMIN_PASSWORD: + description: 'Admin password for DSpace' + required: true + type: string + +runs: + using: "composite" + steps: + + - name: info + shell: bash + run: | + docker ps -a + + - uses: actions/checkout@v4 + with: + repository: dataquest-dev/dspace-import + ref: 'main' + submodules: 'recursive' + path: 'dspace-import' + + + - name: stop and remove containers + id: import + shell: bash + working-directory: dspace-import/scripts + env: + DATADIR: ${{ inputs.DATADIR }} + DB5PORT: 15432 + DB5NAME: dspace-import-db5 + DB7PORT: 543${{ inputs.INSTANCE }} + BEURL: http://dev-5.pc:8${{ inputs.INSTANCE }}/repository/server/api + BEUSERNAME: dspace.admin.dev@dataquest.sk + BEUSERPASSWORD: ${{ inputs.ADMIN_PASSWORD }} + run: | + docker stop $DB5NAME || true + echo "=====" + echo Starting import DB + # create otherwise it will be created with root owner + cid=$(docker run -d --rm --name $DB5NAME -v $(pwd):/dq/scripts -v $DATADIR/dump:/dq/dump -p 127.0.0.1:$DB5PORT:5432 -e POSTGRES_DB=empty -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=dspace postgres /bin/bash -c "cd /dq/scripts && ./init.dspacedb5.sh") + echo "cid=$cid" >> $GITHUB_OUTPUT + sleep 60 + # copy assetstore + echo Preparing assetstore + if [[ -z ${{ inputs.ASSETSTORE }} ]]; then + echo Location of assetstore folder is empty. Not copping assetstore + else + docker cp ${{ inputs.ASSETSTORE }} dspace${{ inputs.INSTANCE }}:/dspace/ + fi + echo "=====" + cd ../ + pip install -r requirements.txt || true + echo "=====" + cd ./src + # cleanup resume + rm __temp/resume/*.json || true + # arguments of the script + # required + echo "=====" + # Generate the current timestamp for the log filename + timestamp=$(date +"%y%m%d%H%M") + log_file_timestamp="${{ inputs.LOGDIR }}python.${timestamp}.log" + echo $log_file_timestamp + mkdir ../__logs + # Continuously append to the log file $log_file_timestamp. Start appending when the first file is added to the __logs folder. + inotifywait -m -e create --format "%w%f" ../__logs/ | while IFS= read -r file; do tail -F --retry "$file" >> "$log_file_timestamp" 2>/dev/null & done & + echo "=====" + args=( + --resume=false + --config=backend.endpoint=$BEURL + --config=backend.user=$BEUSERNAME + --config=backend.password=$BEUSERPASSWORD + --config=db_dspace_7.port=$DB7PORT + --config=db_dspace_5.port=$DB5PORT + --config=db_utilities_5.port=$DB5PORT + --config=input.datadir=$DATADIR/data/ + --config=input.icondir=$DATADIR/icon/ + ) + # Add --resume argument if inputs.ASSETSTORE is defined + if [ -n "${{ inputs.ASSETSTORE }}" ]; then + args+=(--assetstore=${{ inputs.ASSETSTORE }}) + fi + python3 repo_import.py "${args[@]}" + - name: cleanup + shell: bash + run: | + docker stop ${{ steps.import.outputs.cid }} || true + if: ${{ always() }} + diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000000..c0735383379 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,344 @@ +# GitHub Copilot Instructions for dspace-angular + +## Project Overview + +**dspace-angular** is the Angular-based frontend UI for DSpace 7, a turnkey digital repository application. This is a fork customized for [clarin-dspace](https://github.com/ufal/clarin-dspace/tree/clarin-v7), built with Angular 15, TypeScript 4.8, and Angular Universal for server-side rendering (SSR). + +- **Size**: ~2,800 TypeScript files, 813 components, 805 test files +- **Framework**: Angular 15 with Angular Universal (SSR) +- **Language**: TypeScript 4.8 +- **Package Manager**: Yarn 1.x (NOT npm) +- **Build Tool**: Angular CLI with custom webpack config +- **Testing**: Jasmine/Karma (unit), Cypress (e2e) +- **Target Runtimes**: Node.js 16.x or 18.x +- **Main Branch**: `clarin-v7` + +## Critical Build Requirements + +### Node Version Requirements +**ALWAYS use Node.js 16.x or 18.x**. Node 20+ is NOT compatible due to dependency constraints (eslint-plugin-jsdoc requires Node 14-19). + +```bash +# If using nvm (RECOMMENDED): +nvm install 18 +nvm use 18 +``` + +### Environment Setup (REQUIRED BEFORE ANY BUILD) + +1. **Always install dependencies first**: + ```bash + yarn install --frozen-lockfile + ``` + - Use `--frozen-lockfile` to ensure consistent versions + - If Cypress download fails due to network restrictions, use: `CYPRESS_INSTALL_BINARY=0 yarn install --frozen-lockfile` + +2. **Never use npm** - this project uses Yarn 1.x exclusively. Package manager commands: + - Add dependency: `yarn add ` + - Remove dependency: `yarn remove ` + - Upgrade dependency: `yarn upgrade ` + +## Build & Validation Commands + +### Standard Workflow (ALWAYS run in this order) +```bash +# 1. Install dependencies (required after any branch switch or pull) +yarn install --frozen-lockfile + +# 2. Lint the code (MUST pass - ~76 seconds) +yarn run lint --quiet + +# 3. Check for circular dependencies (MUST pass - ~34 seconds) +yarn run check-circ-deps + +# 4. Build for production (~6.5 minutes) +yarn run build:prod + +# 5. Run unit tests (~3.5 minutes, 4801 tests) +yarn run test:headless +``` + +### Alternative Commands +- **Development server**: `yarn run start:dev` (localhost:4000) +- **Production server**: `yarn start` (builds then runs with SSR) +- **Build only**: `yarn run build:prod` +- **Unit tests with watch**: `yarn run test:watch` +- **E2E tests**: `yarn run e2e` (requires running DSpace REST backend) +- **Clean everything**: `yarn run clean` (removes node_modules, requires reinstall) +- **Clean build artifacts**: `yarn run clean:prod` + +### Build Time Expectations +- Lint: ~76 seconds +- Circular dependency check: ~34 seconds +- Production build: ~6.5 minutes (390+ seconds) +- Unit tests: ~3.5 minutes (212 seconds), 4801 tests +- Full CI pipeline: ~15-20 minutes + +### Common Build Issues & Solutions + +**Issue**: Node version incompatibility +- **Error**: `The engine "node" is incompatible with this module` +- **Solution**: Switch to Node 16.x or 18.x using nvm + +**Issue**: Cypress installation failure +- **Error**: `The Cypress App could not be downloaded` +- **Solution**: Use `CYPRESS_INSTALL_BINARY=0` flag during yarn install + +**Issue**: Out of memory during build +- **Solution**: The CI sets `NODE_OPTIONS='--max-old-space-size=4096'` + +**Issue**: Merge conflicts in yarn.lock +- **Solution**: + ```bash + git checkout --theirs yarn.lock + yarn install + git add yarn.lock + git commit + ``` + +## Code Quality Requirements (CI CHECKS) + +All pull requests MUST pass these checks: + +1. **ESLint validation**: `yarn run lint --quiet` (MUST pass) +2. **No circular dependencies**: `yarn run check-circ-deps` (MUST pass) +3. **TypeDoc comments**: All new/modified public methods and classes require JSDoc comments +4. **Unit tests**: All tests must pass, new features require new tests +5. **Code coverage**: Currently ~77% statements, 74% functions +6. **Build success**: Production build must complete without errors + +### Linting Configuration +- Config file: `.eslintrc.json` +- Rules: TypeScript strict mode, Angular style guide +- Auto-fix: `yarn run lint-fix` (use cautiously) + +## Project Structure + +### Root Directory Files +``` +angular.json # Angular CLI configuration +karma.conf.js # Karma test runner config +cypress.config.ts # Cypress e2e test config +package.json # Dependencies & scripts +tsconfig.json # TypeScript base config +.eslintrc.json # ESLint rules +nodemon.json # Development server hot reload +server.ts # Express server for SSR +``` + +### Key Directories +``` +/config # App configuration (config.yml) +/src/app # Main application code (components, services, modules) +/src/assets # Static files (i18n, images, fonts) +/src/config # TypeScript config interfaces +/src/environments # Environment-specific configs +/src/themes # UI themes (dspace, custom) +/cypress/e2e # E2E integration tests +/webpack # Custom webpack configurations +/docker # Docker compose files for dev/test +/scripts # Build and utility scripts +/docs # Documentation (Configuration.md) +``` + +### Source Code Organization +- **Components**: `/src/app//.component.ts` +- **Services**: `/src/app/core/services/` +- **Models**: `/src/app/core/shared/` +- **Tests**: Co-located with source files (`.spec.ts`) +- **Themes**: `/src/themes/dspace/` and `/src/themes/custom/` + +### Configuration Files + +**Application Configuration**: +- Default: `config/config.yml` +- Environment-specific: `config/config.(dev|prod).yml` +- Runtime: Can be overridden via environment variables (prefix: `DSPACE_`) + +**Environment Variables** (for testing/CI): +```bash +DSPACE_REST_HOST=localhost +DSPACE_REST_PORT=8080 +DSPACE_REST_NAMESPACE=/server +DSPACE_REST_SSL=false +DSPACE_UI_HOST=localhost +DSPACE_UI_PORT=4000 +``` + +## Testing + +### Unit Tests (Jasmine/Karma) +- **Location**: `src/**/*.spec.ts` (co-located with source) +- **Run**: `yarn run test:headless` (CI mode) or `yarn test` (watch mode) +- **Run single test**: `yarn run test:headless --include='**/path/to/test.spec.ts'` + - Use glob patterns relative to workspace root + - Example: `yarn run test:headless --include='**/date-picker/date-picker.component.spec.ts'` +- **Config**: `karma.conf.js` +- **Coverage**: Generated in `/coverage/dspace-angular/` +- **Expectations**: ~4801 tests, ~77% coverage, ~3.5 minute runtime + +### E2E Tests (Cypress) +- **Location**: `/cypress/e2e/**/*.spec.ts` +- **Run**: `yarn run e2e` (requires backend at localhost:8080) +- **Config**: `cypress.config.ts` +- **Prerequisites**: + - DSpace REST API running (see docker/README.md) + - Test data loaded (Entities dataset) +- **Browser**: Chrome by default + +### Test Best Practices +- Always run unit tests before creating a PR +- Co-locate test files with source code +- Use existing test patterns for consistency +- E2E tests require backend - document this in PRs + +## GitHub Actions CI Workflow + +**Main workflow**: `.github/workflows/build.yml` + +CI runs on: +- Push to `clarin-v7` or `customer/*` branches +- All pull requests +- Daily schedule +- Manual workflow dispatch + +**CI Steps** (matrix: Node 16.x, 18.x): +1. Checkout code +2. Install Node.js (specified version) +3. Install Chrome (for e2e tests) +4. Cache Yarn dependencies +5. Install dependencies: `yarn install --frozen-lockfile` +6. Lint: `yarn run lint --quiet` +7. Check circular deps: `yarn run check-circ-deps` +8. Build: `yarn run build:prod` +9. Unit tests: `yarn run test:headless` +10. Start Docker backend (for e2e) +11. E2E tests with Cypress +12. Verify SSR rendering + +**Artifacts**: Coverage reports, e2e videos/screenshots (on failure) + +## Docker Support + +Docker configurations are in `/docker/` directory (see docker/README.md): + +- `docker-compose.yml`: Start Angular UI from current branch +- `docker-compose-rest.yml`: Start DSpace REST API backend +- `docker-compose-ci.yml`: CI testing configuration +- `cli.yml`: DSpace CLI commands +- `cli.assetstore.yml`: Load test data + +**Note**: Docker images are for development/testing only, NOT production-ready. + +## Architecture & Patterns + +### Angular Universal (SSR) +- Server entry: `src/main.server.ts` +- Browser entry: `src/main.browser.ts` +- Express server: `server.ts` +- SSR is REQUIRED - all changes must support SSR + +### State Management +- Uses NgRx for state management +- Store configuration: `/src/app/store/` + +### Theming +- Themeable components in `/src/themes/` +- Base theme: `dspace` +- Custom theme: `custom` + +### Key Dependencies +- Angular 15.2.8 +- TypeScript 4.8.4 +- Bootstrap 4.6.x +- NgRx 15.4.x +- RxJS 7.8.x + +## Making Code Changes + +### Before Starting +1. Switch to Node 16 or 18: `nvm use 18` +2. Install dependencies: `yarn install --frozen-lockfile` +3. Run linter baseline: `yarn run lint --quiet` +4. Run tests baseline: `yarn run test:headless` + +### During Development +1. Make minimal, focused changes +2. Add TypeDoc comments for public APIs +3. Keep changes SSR-compatible +4. Run lint frequently: `yarn run lint --quiet` +5. Add/update tests as needed + +### Before Committing +1. Lint: `yarn run lint --quiet` (MUST pass) +2. Check circular deps: `yarn run check-circ-deps` (MUST pass) +3. Build: `yarn run build:prod` (MUST succeed) +4. Test: `yarn run test:headless` (MUST pass) +5. Manual smoke test if UI changes made + +### Pull Request Requirements +- PR must pass all CI checks +- Include description of changes +- Link to related issues +- Update documentation if needed +- Add tests for new functionality +- Follow Angular style guide + +## Common Patterns & Conventions + +### File Naming +- Components: `*.component.ts` +- Services: `*.service.ts` +- Models: `*.model.ts` +- Tests: `*.spec.ts` +- Module: `*.module.ts` + +### Code Style +- Prefix for components: `ds-` (e.g., ``) +- TypeScript strict mode enabled +- Single quote for strings +- 2-space indentation +- No trailing spaces +- End files with newline + +### Import Conventions +- Avoid barrel imports where possible +- Import from specific files for better tree-shaking +- Use lodash method imports: `import get from 'lodash/get'` + +## Troubleshooting Checklist + +If builds/tests fail: + +1. **Check Node version**: `node --version` (should be 16.x or 18.x) +2. **Clean install**: `yarn run clean && yarn install --frozen-lockfile` +3. **Clear Angular cache**: `yarn run clean:cli` +4. **Check for merge conflicts** in yarn.lock +5. **Verify environment variables** if configuration seems wrong +6. **Check disk space** - builds create large artifacts +7. **Increase Node memory** if OOM: `export NODE_OPTIONS='--max-old-space-size=4096'` + +## Additional Resources + +- **Documentation**: https://wiki.lyrasis.org/display/DSDOC7x/ +- **REST API Contract**: https://github.com/DSpace/RestContract +- **Contributing Guide**: CONTRIBUTING.md +- **Configuration Guide**: docs/Configuration.md +- **Docker Guide**: docker/README.md + +## Important Notes + +- **ALWAYS** use Yarn, never npm +- **ALWAYS** use Node 16 or 18, not 20+ +- **ALWAYS** run `yarn install --frozen-lockfile` after branch changes +- **ALWAYS** validate changes with lint, build, and test before PR +- This project uses Angular Universal - all code must be SSR-compatible +- Default backend: Demo DSpace REST API (no local backend required for basic testing) +- Branch naming: Feature branches should target `clarin-v7` + +## Trust These Instructions + +These instructions have been validated by running all commands successfully. If you encounter discrepancies or errors: +1. First verify you're using Node 16 or 18 +2. Ensure dependencies are installed: `yarn install --frozen-lockfile` +3. Only search for additional information if these instructions are incomplete or proven incorrect diff --git a/.github/disabled-workflows/pull_request_opened.yml b/.github/disabled-workflows/pull_request_opened.yml deleted file mode 100644 index 0dc718c0b9a..00000000000 --- a/.github/disabled-workflows/pull_request_opened.yml +++ /dev/null @@ -1,26 +0,0 @@ -# This workflow runs whenever a new pull request is created -# TEMPORARILY DISABLED. Unfortunately this doesn't work for PRs created from forked repositories (which is how we tend to create PRs). -# There is no known workaround yet. See https://github.community/t/how-to-use-github-token-for-prs-from-forks/16818 -name: Pull Request opened - -# Only run for newly opened PRs against the "main" branch -on: - pull_request: - types: [opened] - branches: - - main - -jobs: - automation: - runs-on: ubuntu-latest - steps: - # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards - # See https://github.com/marketplace/actions/pull-request-assigner - - name: Assign PR to creator - uses: thomaseizinger/assign-pr-creator-action@v1.0.0 - # Note, this authentication token is created automatically - # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - # Ignore errors. It is possible the PR was created by someone who cannot be assigned - continue-on-error: true diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index be15b0a507c..8efe0f8cf86 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,26 +1,16 @@ -## References -_Add references/links to any related issues or PRs. These may include:_ -* Fixes #[issue-number] -* Requires DSpace/DSpace#[pr-number] (if a REST API PR is required to test this) +## Problem description -## Description -Short summary of changes (1-2 sentences). +## Analysis +(Write here, if there is needed describe some specific problem. Erase it, when it is not needed.) +## Problems +(Write here, if some unexpected problems occur during solving issues. Erase it, when it is not needed.) -## Instructions for Reviewers -Please add a more detailed description of the changes made by your PR. At a minimum, providing a bulleted list of changes in your PR is helpful to reviewers. +### Sync verification +If en.json5 or cs.json5 translation files were updated: +- [ ] Run `yarn run sync-i18n -t src/assets/i18n/cs.json5 -i` to synchronize messages, and changes are included in this PR. -List of changes in this PR: -* First, ... -* Second, ... +### Manual Testing (if applicable) +- [ ] Added to [testing scenarios](https://github.com/dataquest-dev/dspace-customers/issues/55) -**Include guidance for how to test or review your PR.** This may include: steps to reproduce a bug, screenshots or description of a new feature, or reasons behind specific changes. - -## Checklist -_This checklist provides a reminder of what we are going to look for when reviewing your PR. You need not complete this checklist prior to creating your PR (draft PRs are always welcome). If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_ - -- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible. -- [ ] My PR passes [TSLint](https://palantir.github.io/tslint/) validation using `yarn run lint` -- [ ] My PR doesn't introduce circular dependencies -- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods. -- [ ] My PR passes all specs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide). -- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation. +### Copilot review +- [ ] Requested review from Copilot \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 539fd740ee3..d08eaf48708 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,8 +3,20 @@ # https://docs.github.com/en/free-pro-team@latest/actions/guides/building-and-testing-nodejs name: Build -# Run this Build for all pushes / PRs to current branch -on: [push, pull_request] +# Run this Build for pushes to our main and all PRs +on: + push: + branches: + - dtq-dev + - customer/* + schedule: + - cron: '0 */4 * * *' + pull_request: + workflow_dispatch: + +permissions: + contents: read # to fetch code (actions/checkout) + packages: read # to fetch private images from GitHub Container Registry (GHCR) jobs: tests: @@ -12,28 +24,49 @@ jobs: env: # The ci step will test the dspace-angular code against DSpace REST. # Direct that step to utilize a DSpace REST service that has been started in docker. - DSPACE_REST_HOST: localhost + # Spin up UI on 127.0.0.1 to avoid host resolution issues in e2e tests with Node 18+ + INSTANCE: '2' + DSPACE_CI_IMAGE: 'dataquest/dspace:dspace-7_x-test' + DSPACE_SOLR_IMAGE: dataquest/dspace-solr:dspace-7_x + DSPACE_REST_HOST: 127.0.0.1 DSPACE_REST_PORT: 8080 DSPACE_REST_NAMESPACE: '/server' DSPACE_REST_SSL: false + # Spin up UI on 127.0.0.1 to avoid host resolution issues in e2e tests with Node 18+ + DSPACE_UI_HOST: 127.0.0.1 + DSPACE_UI_PORT: 4000 + # Ensure all SSR caching is disabled in test environment + DSPACE_CACHE_SERVERSIDE_BOTCACHE_MAX: 0 + DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0 + # Tell Cypress to run e2e tests using the same UI URL + CYPRESS_BASE_URL: http://127.0.0.1:4000 + # Disable the cookie consent banner in e2e tests to avoid errors because of elements hidden by it + DSPACE_INFO_ENABLECOOKIECONSENTPOPUP: false # When Chrome version is specified, we pin to a specific version of Chrome # Comment this out to use the latest release #CHROME_VERSION: "90.0.4430.212-1" + # Bump Node heap size (OOM in CI after upgrading to Angular 15) + NODE_OPTIONS: '--max-old-space-size=4096' + # Project name to use when running "docker compose" prior to e2e tests + COMPOSE_PROJECT_NAME: 'ci' + # Docker Registry to use for Docker compose scripts below. + # We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub. + DOCKER_REGISTRY: ghcr.io strategy: # Create a matrix of Node versions to test against (in parallel) matrix: - node-version: [12.x, 14.x] + node-version: [18.x, 20.x] # Do NOT exit immediately if one matrix job fails fail-fast: false # These are the actual CI steps to perform per job steps: # https://github.com/actions/checkout - name: Checkout codebase - uses: actions/checkout@v2 + uses: actions/checkout@v4 # https://github.com/actions/setup-node - name: Install Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} @@ -56,9 +89,9 @@ jobs: # https://github.com/actions/cache/blob/main/examples.md#node---yarn - name: Get Yarn cache directory id: yarn-cache-dir-path - run: echo "::set-output name=dir::$(yarn cache dir)" + run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT - name: Cache Yarn dependencies - uses: actions/cache@v2 + uses: actions/cache@v4 with: # Cache entire Yarn cache directory (see previous step) path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -70,7 +103,10 @@ jobs: run: yarn install --frozen-lockfile - name: Run lint - run: yarn run lint + run: yarn run lint --quiet + + - name: Check for circular dependencies + run: yarn run check-circ-deps - name: Run build run: yarn run build:prod @@ -78,62 +114,81 @@ jobs: - name: Run specs (unit tests) run: yarn run test:headless + # Upload code coverage report to artifact (for one version of Node only), + # so that it can be shared with the 'codecov' job (see below) # NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286 - # Upload coverage reports to Codecov (for Node v12 only) - # https://github.com/codecov/codecov-action - - name: Upload coverage to Codecov.io - uses: codecov/codecov-action@v2 - if: matrix.node-version == '12.x' + - name: Upload code coverage report to Artifact + uses: actions/upload-artifact@v4 + if: matrix.node-version == '18.x' + with: + name: coverage-report-${{ matrix.node-version }} + path: 'coverage/dspace-angular/lcov.info' + retention-days: 14 - # Using docker-compose start backend using CI configuration + # Login to our Docker registry, so that we can access private Docker images using "docker compose" below. + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Using "docker compose" start backend using CI configuration # and load assetstore from a cached copy - name: Start DSpace REST Backend via Docker (for e2e tests) run: | - docker-compose -f ./docker/docker-compose-ci.yml up -d - docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli + docker compose -f ./docker/docker-compose-ci.yml up -d + docker compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli docker container ls # Run integration tests via Cypress.io # https://github.com/cypress-io/github-action # (NOTE: to run these e2e tests locally, just use 'ng e2e') - name: Run e2e tests (integration tests) - uses: cypress-io/github-action@v2 + uses: cypress-io/github-action@v6 with: - # Run tests in Chrome, headless mode + # Run tests in Chrome, headless mode (default) browser: chrome - headless: true # Start app before running tests (will be stopped automatically after tests finish) start: yarn run serve:ssr # Wait for backend & frontend to be available # NOTE: We use the 'sites' REST endpoint to also ensure the database is ready - wait-on: http://localhost:8080/server/api/core/sites, http://localhost:4000 + wait-on: http://127.0.0.1:8080/server/api/core/sites, http://127.0.0.1:4000 # Wait for 2 mins max for everything to respond wait-on-timeout: 120 # Cypress always creates a video of all e2e tests (whether they succeeded or failed) # Save those in an Artifact - name: Upload e2e test videos to Artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: always() with: - name: e2e-test-videos + name: e2e-test-videos-${{ matrix.node-version }} path: cypress/videos # If e2e tests fail, Cypress creates a screenshot of what happened # Save those in an Artifact - name: Upload e2e test failure screenshots to Artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 if: failure() with: - name: e2e-test-screenshots + name: e2e-test-screenshots-${{ matrix.node-version }} path: cypress/screenshots + - name: Stop app (in case it stays up after e2e tests) + run: | + app_pid=$(lsof -t -i:4000) + if [[ ! -z $app_pid ]]; then + echo "App was still up! (PID: $app_pid)" + kill -9 $app_pid + fi + # Start up the app with SSR enabled (run in background) - name: Start app in SSR (server-side rendering) mode run: | nohup yarn run serve:ssr & printf 'Waiting for app to start' - until curl --output /dev/null --silent --head --fail http://localhost:4000/home; do + until curl --output /dev/null --silent --head --fail http://127.0.0.1:4000/home; do printf '.' sleep 2 done @@ -142,14 +197,151 @@ jobs: # Get homepage and verify that the tag includes "DSpace". # If it does, then SSR is working, as this tag is created by our MetadataService. # This step also prints entire HTML of homepage for easier debugging if grep fails. - - name: Verify SSR (server-side rendering) + - name: Verify SSR (server-side rendering) on Homepage + run: | + result=$(wget -O- -q http://127.0.0.1:4000/home) + echo "$result" + echo "$result" | grep -oE "]*>" | grep Home + + # Get a specific community in our test data and verify that the "

" tag includes "Publications" (the community name). + # If it does, then SSR is working. + - name: Verify SSR on a Community page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/communities/0958c910-2037-42a9-81c7-dca80e3892b4) + echo "$result" + echo "$result" | grep -oE "

]*>[^><]*

" | grep Publications + + # Get a specific collection in our test data and verify that the "

" tag includes "Articles" (the collection name). + # If it does, then SSR is working. + - name: Verify SSR on a Collection page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/collections/282164f5-d325-4740-8dd1-fa4d6d3e7200) + echo "$result" + echo "$result" | grep -oE "

]*>[^><]*

" | grep Articles + + # Get a specific publication in our test data and verify that the tag includes + # the title of this publication. If it does, then SSR is working. + - name: Verify SSR on a Publication page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/publication/6160810f-1e53-40db-81ef-f6621a727398) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "An Economic Model of Mortality Salience" + + # Get a specific person in our test data and verify that the tag includes + # the name of the person. If it does, then SSR is working. + - name: Verify SSR on a Person page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/person/b1b2c768-bda1-448a-a073-fc541e8b24d9) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "Simmons, Cameron" + + # Get a specific project in our test data and verify that the tag includes + # the name of the project. If it does, then SSR is working. + - name: Verify SSR on a Project page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/project/46ccb608-a74c-4bf6-bc7a-e29cc7defea9) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "University Research Fellowship" + + # Get a specific orgunit in our test data and verify that the tag includes + # the name of the orgunit. If it does, then SSR is working. + - name: Verify SSR on an OrgUnit page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/orgunit/9851674d-bd9a-467b-8d84-068deb568ccf) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "Law and Development" + + # Get a specific journal in our test data and verify that the tag includes + # the name of the journal. If it does, then SSR is working. + - name: Verify SSR on a Journal page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/journal/d4af6c3e-53d0-4757-81eb-566f3b45d63a) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "Environmental & Architectural Phenomenology" + + # Get a specific journal volume in our test data and verify that the tag includes + # the name of the volume. If it does, then SSR is working. + - name: Verify SSR on a Journal Volume page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/journalvolume/07c6249f-4bf7-494d-9ce3-6ffdb2aed538) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "Environmental & Architectural Phenomenology Volume 28 (2017)" + + # Get a specific journal issue in our test data and verify that the tag includes + # the name of the issue. If it does, then SSR is working. + - name: Verify SSR on a Journal Issue page + run: | + result=$(wget -O- -q http://127.0.0.1:4000/entities/journalissue/44c29473-5de2-48fa-b005-e5029aa1a50b) + echo "$result" + echo "$result" | grep -oE "]*>" | grep "Environmental & Architectural Phenomenology Vol. 28, No. 1" + + # Verify 301 Handle redirect behavior + # Note: /handle/123456789/260 is the same test Publication used by our e2e tests + - name: Verify 301 redirect from '/handle' URLs + run: | + result=$(wget --server-response --quiet http://127.0.0.1:4000/handle/123456789/260 2>&1 | head -1 | awk '{print $2}') + echo "$result" + [[ "$result" -eq "301" ]] + + # Verify 403 error code behavior + - name: Verify 403 error code from '/403' + run: | + result=$(wget --server-response --quiet http://127.0.0.1:4000/403 2>&1 | head -1 | awk '{print $2}') + echo "$result" + [[ "$result" -eq "403" ]] + + # Verify 404 error code behavior + - name: Verify 404 error code from '/404' and on invalid pages + run: | + result=$(wget --server-response --quiet http://127.0.0.1:4000/404 2>&1 | head -1 | awk '{print $2}') + echo "$result" + result2=$(wget --server-response --quiet http://127.0.0.1:4000/invalidurl 2>&1 | head -1 | awk '{print $2}') + echo "$result2" + [[ "$result" -eq "404" && "$result2" -eq "404" ]] + + # Verify 500 error code behavior + - name: Verify 500 error code from '/500' run: | - result=$(wget -O- -q http://localhost:4000/home) + result=$(wget --server-response --quiet http://127.0.0.1:4000/500 2>&1 | head -1 | awk '{print $2}') echo "$result" - echo "$result" | grep -oE "]*>" | grep DSpace + [[ "$result" -eq "500" ]] - name: Stop running app run: kill -9 $(lsof -t -i:4000) - name: Shutdown Docker containers - run: docker-compose -f ./docker/docker-compose-ci.yml down + run: docker compose -f ./docker/docker-compose-ci.yml down + +# # Codecov upload is a separate job in order to allow us to restart this separate from the entire build/test +# # job above. This is necessary because Codecov uploads seem to randomly fail at times. +# # See https://community.codecov.com/t/upload-issues-unable-to-locate-build-via-github-actions-api/3954 +# codecov: +# # Must run after 'tests' job above +# needs: tests +# runs-on: ubuntu-latest +# steps: +# - name: Checkout +# uses: actions/checkout@v4 +# +# # Download artifacts from previous 'tests' job +# - name: Download coverage artifacts +# uses: actions/download-artifact@v4 +# +# # Now attempt upload to Codecov using its action. +# # NOTE: We use a retry action to retry the Codecov upload if it fails the first time. +# # +# # Retry action: https://github.com/marketplace/actions/retry-action +# # Codecov action: https://github.com/codecov/codecov-action +# - name: Upload coverage to Codecov.io +# uses: Wandalen/wretry.action@v1.3.0 +# with: +# action: codecov/codecov-action@v4 +# # Ensure codecov-action throws an error when it fails to upload +# # This allows us to auto-restart the action if an error is thrown +# with: | +# fail_ci_if_error: true +# token: ${{ secrets.CODECOV_TOKEN }} +# # Try re-running action 5 times max +# attempt_limit: 5 +# # Run again in 30 seconds +# attempt_delay: 30000 diff --git a/.github/workflows/codescan.yml b/.github/workflows/codescan.yml new file mode 100644 index 00000000000..1e16f8fcf86 --- /dev/null +++ b/.github/workflows/codescan.yml @@ -0,0 +1,53 @@ +# DSpace CodeQL code scanning configuration for GitHub +# https://docs.github.com/en/code-security/code-scanning +# +# NOTE: Code scanning must be run separate from our default build.yml +# because CodeQL requires a fresh build with all tests *disabled*. +name: "Code Scanning" + +# Run this code scan for all pushes / PRs to main or maintenance branches. Also run once a week. +on: + push: + branches: + - main + - 'dspace-**' + pull_request: + branches: + - main + - 'dspace-**' + # Don't run if PR is only updating static documentation + paths-ignore: + - '**/*.md' + - '**/*.txt' + schedule: + - cron: "37 0 * * 1" + +jobs: + analyze: + name: Analyze Code + runs-on: ubuntu-latest + # Limit permissions of this GitHub action. Can only write to security-events + permissions: + actions: read + contents: read + security-events: write + + steps: + # https://github.com/actions/checkout + - name: Checkout repository + uses: actions/checkout@v4 + + # Initializes the CodeQL tools for scanning. + # https://github.com/github/codeql-action + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: javascript + + # Autobuild attempts to build any compiled languages + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + # Perform GitHub Code Scanning. + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/create_bitstreams.yml b/.github/workflows/create_bitstreams.yml new file mode 100644 index 00000000000..793bb806445 --- /dev/null +++ b/.github/workflows/create_bitstreams.yml @@ -0,0 +1,32 @@ +name: create_bitstreams - import test files + +on: + workflow_dispatch: + inputs: + INSTANCE: + required: true + default: '8' + type: choice + options: + - '2' + - '5' + - '6' + - '8' + +jobs: + import-specific-bitstreams: + runs-on: dspace-dep-1 + env: + DSPACE_REST_API: http://dev-5.pc:8${{ github.event.inputs.INSTANCE }}/server/api + steps: + - uses: actions/checkout@v4 + with: + repository: dataquest-dev/dspace-rest-test + ref: master + submodules: 'recursive' + + - name: install requirements and run import + run: | + pip install -q -r requirements.txt + cd tests/integration + python3 create_bitstreams.py diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 00000000000..e6104dc8775 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,231 @@ +# DSpace Docker deploy on dataquest servers +name: Deploy DSpace + +on: + workflow_call: + inputs: + INSTANCE: + required: false + type: string + default: '5' + IMPORT: + required: false + default: true + type: boolean + ERASE_DB: + required: false + default: false + type: boolean + + workflow_dispatch: + inputs: + INSTANCE: + required: true + default: '5' + type: choice + options: + - '*' + - '5' + - '8' + IMPORT: + required: true + default: true + type: boolean + ERASE_DB: + required: false + default: false + type: boolean + +jobs: + deploy-5: + if: inputs.INSTANCE == '*' || inputs.INSTANCE == '5' + runs-on: dspace-dep-1 + timeout-minutes: 10 + env: + INSTANCE: '5' + ENVFILE: /opt/dspace-envs/.env.dspace.dev-5 + ADMIN_PASSWORD: ${{ secrets.DSPACE_ADMIN_PASSWORD }} + USER_PASSWORD: ${{ secrets.DSPACE_USER_PASSWORD }} + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/erase-db + if: inputs.ERASE_DB + with: + INSTANCE: ${{ env.INSTANCE }} + NAME: dspace-${{ env.INSTANCE }} + + - name: deploy to dev-5 + working-directory: build-scripts/run/ + run: | + ./start.sh dspace-$INSTANCE + cd ../.. + # use customized docker-compose file for the `85` port because the server is exposed with the namespace `repository` + # `/repository/server` + docker compose --env-file $ENVFILE -p dspace-$INSTANCE -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f /opt/dspace-envs/5/docker-compose-rest.yml -f /opt/dspace-envs/5/docker-compose.yml up -d --no-build + + deploy-8: + if: inputs.INSTANCE == '*' || inputs.INSTANCE == '8' + runs-on: dspace-dep-1 + timeout-minutes: 120 + env: + INSTANCE: '8' + # 2024/02: this .env replaces ENTRYPOINT to angular + # !!!!WARNING!!!! + # disable TSL checks = allowing to cooperate with https backend with invalid + # certificate + ENVFILE: /opt/dspace-envs/.env.dspace.imported.dev-5 + ADMIN_PASSWORD: ${{ secrets.DSPACE_ADMIN_PASSWORD }} + USER_PASSWORD: ${{ secrets.DSPACE_USER_PASSWORD }} + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/actions/erase-db + if: inputs.ERASE_DB + with: + INSTANCE: ${{ env.INSTANCE }} + NAME: dspace-${{ env.INSTANCE }} + + - name: deploy dspace-import on dev-5 + working-directory: build-scripts/run/ + run: | + ./start.sh dspace-$INSTANCE + cd ../.. + # this is not necessary, since extra.yml doesn't contain any new images that weren't pulled within script above + # docker compose --env-file $ENVFILE -p dspace-$INSTANCE -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f /opt/dspace-envs/8/extra.yml pull + docker compose --env-file $ENVFILE -p dspace-$INSTANCE -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f /opt/dspace-envs/8/extra.yml up -d --no-build + + # this must be here and not in the start.sh, because extra.yml replaces dspace container + ## !!!!! please remove this section if you do not want handle server !!!!!!! + echo "=====" + echo "installing handle server" + docker exec dspace${INSTANCE} /dspace/bin/make-handle-config + echo "made handle config:" + docker exec dspace${INSTANCE} cat /dspace/handle-server/config.dct + docker exec dspace${INSTANCE} /dspace/bin/start-handle-server + echo "started handle server" + # this seems to be the easiest solution for now + docker restart dockerized-nginx-with-shibboleth-nginx-1 + /bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://dev-5.pc:8$INSTANCE/repository/server/api)" != "200" ]]; do sleep 5; done' + + + import-8: + runs-on: dspace-dep-1 + if: inputs.IMPORT + needs: deploy-8 + env: + INSTANCE: '8' + ENVFILE: /opt/dspace-envs/.env.dspace.imported.dev-5 + steps: + - uses: ./.github/actions/import-db + with: + INSTANCE: ${{ env.INSTANCE }} + DATADIR: /opt/dspace-data/clarin-dspace/ + ASSETSTORE: /opt/dspace-data/clarin-dspace/assetstore/ + LOGDIR: /log/ + ADMIN_PASSWORD: ${{ secrets.DSPACE_ADMIN_PASSWORD }} + + - name: dspace basic command + run: | + export DNAME=dspace$INSTANCE + docker logs -n 50 $DNAME + + echo "dspace version:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace version" + + echo "dspace cleanup:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace cleanup -v" + + echo "dspace reindex solr:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace index-discovery -b" + + echo "dspace reindex OAI-PMH:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace oai import -c" + + echo "dspace checker:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace checker -v -l" + + - name: dspace healthcheck + run: | + export DNAME=dspace$INSTANCE + echo "dspace healthcheck:" + docker exec $DNAME /bin/bash -c "cd /dspace/bin && ./dspace healthcheck -v" + + playwright-after-deploy8: + needs: deploy-8 + if: '!inputs.IMPORT' + uses: ./.github/workflows/playwright-tests.yml + secrets: inherit + + rest-tests-after-deploy8: + runs-on: ubuntu-latest + needs: playwright-after-deploy8 + timeout-minutes: 120 + steps: + - name: run rest-tests + run: | + curl -H "Accept: application/vnd.github.everest-preview+json" \ + -H "Authorization: token ${{ secrets.DEPLOY_DEV5_GH_ACTION_DISPATCH }}" \ + --request POST \ + https://api.github.com/repos/dataquest-dev/\ + dspace-rest-test/actions/workflows/run_unittests.yml/dispatches \ + --data "{\"ref\":\"refs/heads/master\"}" 2> /dev/null + + # wait for it to start + sleep 30s + + # get result of last job + RES=$(curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${{ secrets.DEPLOY_DEV5_GH_ACTION_DISPATCH }}" https://api.github.com/repos/dataquest-dev/dspace-rest-test/actions/workflows/run_unittests.yml/runs?per_page=1 2> /dev/null | jq .workflow_runs[0].conclusion) + + # while job did not finish, sleep + while [[ $RES == 'null' ]]; do + sleep 10s + RES=$(curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${{ secrets.DEPLOY_DEV5_GH_ACTION_DISPATCH }}" https://api.github.com/repos/dataquest-dev/dspace-rest-test/actions/workflows/run_unittests.yml/runs?per_page=1 2> /dev/null | jq .workflow_runs[0].conclusion) + done; + + echo $RES + # if last result is not success, return -1 and fail + if [[ $RES != \"success\" ]]; then + echo "rest-tests have failed! check appropriate action run in the dspace-rest-test repository" + exit 1 + fi; + + + playwright-after-import8: + needs: import-8 + if: inputs.IMPORT + uses: ./.github/workflows/playwright-tests.yml + secrets: inherit + + rest-tests-after-import8: + runs-on: ubuntu-latest + needs: playwright-after-import8 + timeout-minutes: 120 + steps: + - name: run rest-tests + run: | + curl -H "Accept: application/vnd.github.everest-preview+json" \ + -H "Authorization: token ${{ secrets.DEPLOY_DEV5_GH_ACTION_DISPATCH }}" \ + --request POST \ + https://api.github.com/repos/dataquest-dev/\ + dspace-rest-test/actions/workflows/run_unittests.yml/dispatches \ + --data "{\"ref\":\"refs/heads/master\"}" 2> /dev/null + + # wait for it to start + sleep 30s + + # get result of last job + RES=$(curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${{ secrets.DEPLOY_DEV5_GH_ACTION_DISPATCH }}" https://api.github.com/repos/dataquest-dev/dspace-rest-test/actions/workflows/run_unittests.yml/runs?per_page=1 2> /dev/null | jq .workflow_runs[0].conclusion) + + # while job did not finish, sleep + while [[ $RES == 'null' ]]; do + sleep 10s + RES=$(curl -H "Accept: application/vnd.github.everest-preview+json" -H "Authorization: token ${{ secrets.DEPLOY_DEV5_GH_ACTION_DISPATCH }}" https://api.github.com/repos/dataquest-dev/dspace-rest-test/actions/workflows/run_unittests.yml/runs?per_page=1 2> /dev/null | jq .workflow_runs[0].conclusion) + done; + + echo $RES + # if last result is not success, return -1 and fail + if [[ $RES != \"success\" ]]; then + echo "rest-tests have failed! check appropriate action run in the dspace-rest-test repository" + exit 1 + fi; diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 00ec2fa8f79..711cd674450 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,76 +3,70 @@ name: Docker images # Run this Build for all pushes to 'main' or maintenance branches, or tagged releases. # Also run for PRs to ensure PR doesn't break Docker build process +# NOTE: uses "reusable-docker-build.yml" in DSpace/DSpace to actually build each of the Docker images +# https://github.com/DSpace/DSpace/blob/dspace-7_x/.github/workflows/reusable-docker-build.yml +# on: push: branches: - - main - - 'dspace-**' - tags: - - 'dspace-**' + - dtq-dev + - customer/* pull_request: + workflow_dispatch: + +permissions: + contents: read # to fetch code (actions/checkout) + packages: write # to write images to GitHub Container Registry (GHCR) jobs: - docker: + ############################################################# + # Build/Push the 'dspace/dspace-angular' image + ############################################################# + dspace-angular: # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular' - if: github.repository == 'dspace/dspace-angular' - runs-on: ubuntu-latest - env: - # Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action) - # For a new commit on default branch (main), use the literal tag 'dspace-7_x' on Docker image. - # For a new commit on other branches, use the branch name as the tag for Docker image. - # For a new tag, copy that tag name as the tag for Docker image. - IMAGE_TAGS: | - type=raw,value=dspace-7_x,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }} - type=ref,event=tag - # Define default tag "flavor" for docker/metadata-action per - # https://github.com/docker/metadata-action#flavor-input - # We turn off 'latest' tag by default. - TAGS_FLAVOR: | - latest=false - - steps: - # https://github.com/actions/checkout - - name: Checkout codebase - uses: actions/checkout@v2 - - # https://github.com/docker/setup-buildx-action - - name: Setup Docker Buildx - uses: docker/setup-buildx-action@v1 - - # https://github.com/docker/login-action - - name: Login to DockerHub - # Only login if not a PR, as PRs only trigger a Docker build and not a push - if: github.event_name != 'pull_request' - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + if: github.repository == 'dataquest-dev/dspace-angular' + # Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image + uses: dataquest-dev/DSpace/.github/workflows/reusable-docker-build.yml@dtq-dev + with: + build_id: dspace-angular-dev + image_name: dataquest/dspace-angular + dockerfile_path: ./Dockerfile + tags_flavor: suffix=-dev + # As this is a "dev" image, its tags are all suffixed with "-dev". Otherwise, it uses the same + # tagging logic as the primary 'dspace/dspace-angular' image above. + run_python_version_script: true + python_version_script_dest: src/static-files/VERSION_D.html + secrets: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }} - ############################################### - # Build/Push the 'dspace/dspace-angular' image - ############################################### - # https://github.com/docker/metadata-action - # Get Metadata for docker_build step below - - name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image - id: meta_build - uses: docker/metadata-action@v3 - with: - images: dspace/dspace-angular - tags: ${{ env.IMAGE_TAGS }} - flavor: ${{ env.TAGS_FLAVOR }} + ############################################################# + # Build/Push the 'dataquest/dspace-angular' image ('-dist' tag) + ############################################################# + dspace-angular-dist: + # Ensure this job never runs on forked repos. It's only executed for 'dataquest/dspace-angular' + if: github.repository == 'dataquest-dev/dspace-angular' + # Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image + uses: dataquest-dev/DSpace/.github/workflows/reusable-docker-build.yml@dtq-dev + with: + build_id: dspace-angular + image_name: dataquest/dspace-angular + dockerfile_path: ./Dockerfile.dist + run_python_version_script: true + python_version_script_dest: src/static-files/VERSION_D.html + secrets: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_ACCESS_TOKEN: ${{ secrets.DOCKER_ACCESS_TOKEN }} + # Enable redeploy of sandbox & demo if the branch for this image matches the deployment branch of + # these sites as specified in reusable-docker-build.xml + REDEPLOY_SANDBOX_URL: ${{ secrets.REDEPLOY_SANDBOX_URL }} + REDEPLOY_DEMO_URL: ${{ secrets.REDEPLOY_DEMO_URL }} - # https://github.com/docker/build-push-action - - name: Build and push 'dspace-angular' image - id: docker_build - uses: docker/build-push-action@v2 - with: - context: . - file: ./Dockerfile - # For pull requests, we run the Docker build (to ensure no PR changes break the build), - # but we ONLY do an image push to DockerHub if it's NOT a PR - push: ${{ github.event_name != 'pull_request' }} - # Use tags / labels provided by 'docker/metadata-action' above - tags: ${{ steps.meta_build.outputs.tags }} - labels: ${{ steps.meta_build.outputs.labels }} + deploy: + needs: dspace-angular + uses: dataquest-dev/dspace-angular/.github/workflows/deploy.yml@dtq-dev + if: ${{ github.event_name != 'pull_request' }} + with: + INSTANCE: '5' + IMPORT: false + secrets: inherit diff --git a/.github/workflows/erase_db.yml b/.github/workflows/erase_db.yml new file mode 100644 index 00000000000..f3526c680dd --- /dev/null +++ b/.github/workflows/erase_db.yml @@ -0,0 +1,29 @@ +name: Erase database + +on: + workflow_dispatch: + inputs: + INSTANCE: + required: true + default: '8' + type: choice + options: + - '5' + - '8' + - '4' + + +jobs: + erase_db: + runs-on: dspace-dep-1 + timeout-minutes: 5 + env: + INSTANCE: ${{ inputs.INSTANCE }} + steps: + + - uses: actions/checkout@v4 + + - uses: ./.github/actions/erase-db + with: + INSTANCE: ${{ env.INSTANCE }} + NAME: dspace-${{ env.INSTANCE }} diff --git a/.github/workflows/import-weekly.yml b/.github/workflows/import-weekly.yml new file mode 100644 index 00000000000..53e32a9f46e --- /dev/null +++ b/.github/workflows/import-weekly.yml @@ -0,0 +1,15 @@ +name: Import Weekly + +on: + schedule: + - cron: '0 0 * * 0' # every Sunday at midnight UTC + workflow_dispatch: + +jobs: + deploy: + uses: ./.github/workflows/deploy.yml + with: + INSTANCE: '8' + IMPORT: true + ERASE_DB: true + secrets: inherit diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml index 6b9a273ab6d..e69de29bb2d 100644 --- a/.github/workflows/issue_opened.yml +++ b/.github/workflows/issue_opened.yml @@ -1,29 +0,0 @@ -# This workflow runs whenever a new issue is created -name: Issue opened - -on: - issues: - types: [opened] - -jobs: - automation: - runs-on: ubuntu-latest - steps: - # Add the new issue to a project board, if it needs triage - # See https://github.com/marketplace/actions/create-project-card-action - - name: Add issue to project board - # Only add to project board if issue is flagged as "needs triage" or has no labels - # NOTE: By default we flag new issues as "needs triage" in our issue template - if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '') - uses: technote-space/create-project-card-action@v1 - # Note, the authentication token below is an ORG level Secret. - # It must be created/recreated manually via a personal access token with "public_repo" and "admin:org" permissions - # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token - # This is necessary because the "DSpace Backlog" project is an org level project (i.e. not repo specific) - with: - GITHUB_TOKEN: ${{ secrets.ORG_PROJECT_TOKEN }} - PROJECT: DSpace Backlog - COLUMN: Triage - CHECK_ORG_PROJECT: true - # Ignore errors - continue-on-error: true diff --git a/.github/workflows/label_merge_conflicts.yml b/.github/workflows/label_merge_conflicts.yml index dcbab18f1b5..ccc6c401c0b 100644 --- a/.github/workflows/label_merge_conflicts.yml +++ b/.github/workflows/label_merge_conflicts.yml @@ -1,25 +1,39 @@ # This workflow checks open PRs for merge conflicts and labels them when conflicts are found name: Check for merge conflicts -# Run whenever the "main" branch is updated -# NOTE: This means merge conflicts are only checked for when a PR is merged to main. +# Run this for all pushes (i.e. merges) to 'main' or maintenance branches on: push: branches: - main + - 'dspace-**' + # So that the `conflict_label_name` is removed if conflicts are resolved, + # we allow this to run for `pull_request_target` so that github secrets are available. + pull_request_target: + types: [ synchronize ] + +permissions: {} jobs: triage: + # Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular' + if: github.repository == 'dspace/dspace-angular' runs-on: ubuntu-latest + permissions: + pull-requests: write steps: - # See: https://github.com/mschilde/auto-label-merge-conflicts/ + # See: https://github.com/prince-chrismc/label-merge-conflicts-action - name: Auto-label PRs with merge conflicts - uses: mschilde/auto-label-merge-conflicts@v2.0 + uses: prince-chrismc/label-merge-conflicts-action@v3 + # Ignore any failures -- may occur (randomly?) for older, outdated PRs. + continue-on-error: true # Add "merge conflict" label if a merge conflict is detected. Remove it when resolved. # Note, the authentication token is created automatically # See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token with: - CONFLICT_LABEL_NAME: 'merge conflict' - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # Ignore errors - continue-on-error: true + conflict_label_name: 'merge conflict' + github_token: ${{ secrets.GITHUB_TOKEN }} + conflict_comment: | + Hi @${author}, + Conflicts have been detected against the base branch. + Please [resolve these conflicts](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/about-merge-conflicts) as soon as you can. Thanks! \ No newline at end of file diff --git a/.github/workflows/new_issue_assign.yml b/.github/workflows/new_issue_assign.yml new file mode 100644 index 00000000000..03c1b28a68a --- /dev/null +++ b/.github/workflows/new_issue_assign.yml @@ -0,0 +1,16 @@ +name: New issue assign +on: + issues: + types: [opened] + +jobs: + add-to-project: + name: Add issue to project + runs-on: ubuntu-latest + steps: + - uses: actions/add-to-project@v0.5.0 + with: + # You can target a project in a different organization + # to the issue + project-url: https://github.com/orgs/dataquest-dev/projects/12 + github-token: ${{ secrets.PAT_ISSUE_MGMT }} diff --git a/.github/workflows/playwright-tests.yml b/.github/workflows/playwright-tests.yml new file mode 100644 index 00000000000..4cfe6f64e45 --- /dev/null +++ b/.github/workflows/playwright-tests.yml @@ -0,0 +1,39 @@ +name: Playwright tests + +on: + workflow_dispatch: + workflow_call: + +jobs: + playwright-tests: + runs-on: dspace-test-1 + timeout-minutes: 45 + steps: + - name: Checkout Playwright tests + uses: actions/checkout@v4 + with: + repository: dataquest-dev/dspace-ui-tests + path: dspace-ui-tests + token: ${{ secrets.DEPLOY_DEV5_GH_ACTION_DISPATCH }} + + - name: Checkout main repository + uses: actions/checkout@v4 + with: + path: this-repository + + - name: Copy customer config to UI tests repository for merging + run: | + cp this-repository/config/config.lindat.ui.tests.json dspace-ui-tests/customer-constants/ + + - name: test + working-directory: dspace-ui-tests/scripts + env: + HOME_URL: https://dev-5.pc:8443/repository/ + NAME: LINDAT + run: | + ./test.sh + + - uses: mazoea/ga-maz/end@master + with: + CHOWN_DIR: ./ + if: ${{ always() }} \ No newline at end of file diff --git a/.github/workflows/port_merged_pull_request.yml b/.github/workflows/port_merged_pull_request.yml new file mode 100644 index 00000000000..857f22755e4 --- /dev/null +++ b/.github/workflows/port_merged_pull_request.yml @@ -0,0 +1,46 @@ +# This workflow will attempt to port a merged pull request to +# the branch specified in a "port to" label (if exists) +name: Port merged Pull Request + +# Only run for merged PRs against the "main" or maintenance branches +# We allow this to run for `pull_request_target` so that github secrets are available +# (This is required when the PR comes from a forked repo) +on: + pull_request_target: + types: [ closed ] + branches: + - main + - 'dspace-**' + +permissions: + contents: write # so action can add comments + pull-requests: write # so action can create pull requests + +jobs: + port_pr: + runs-on: ubuntu-latest + # Don't run on closed *unmerged* pull requests + if: github.event.pull_request.merged + steps: + # Checkout code + - uses: actions/checkout@v4 + # Port PR to other branch (ONLY if labeled with "port to") + # See https://github.com/korthout/backport-action + - name: Create backport pull requests + uses: korthout/backport-action@v2 + with: + # Trigger based on a "port to [branch]" label on PR + # (This label must specify the branch name to port to) + label_pattern: '^port to ([^ ]+)$' + # Title to add to the (newly created) port PR + pull_title: '[Port ${target_branch}] ${pull_title}' + # Description to add to the (newly created) port PR + pull_description: 'Port of #${pull_number} by @${pull_author} to `${target_branch}`.' + # Copy all labels from original PR to (newly created) port PR + # NOTE: The labels matching 'label_pattern' are automatically excluded + copy_labels_pattern: '.*' + # Skip any merge commits in the ported PR. This means only non-merge commits are cherry-picked to the new PR + merge_commits: 'skip' + # Use a personal access token (PAT) to create PR as 'dspace-bot' user. + # A PAT is required in order for the new PR to trigger its own actions (for CI checks) + github_token: ${{ secrets.PR_PORT_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/pull_request_opened.yml b/.github/workflows/pull_request_opened.yml new file mode 100644 index 00000000000..bbac52af243 --- /dev/null +++ b/.github/workflows/pull_request_opened.yml @@ -0,0 +1,24 @@ +# This workflow runs whenever a new pull request is created +name: Pull Request opened + +# Only run for newly opened PRs against the "main" or maintenance branches +# We allow this to run for `pull_request_target` so that github secrets are available +# (This is required to assign a PR back to the creator when the PR comes from a forked repo) +on: + pull_request_target: + types: [ opened ] + branches: + - main + - 'dspace-**' + +permissions: + pull-requests: write + +jobs: + automation: + runs-on: ubuntu-latest + steps: + # Assign the PR to whomever created it. This is useful for visualizing assignments on project boards + # See https://github.com/toshimaru/auto-author-assign + - name: Assign PR to creator + uses: toshimaru/auto-author-assign@v2.1.0 diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml new file mode 100644 index 00000000000..5717f1a89f2 --- /dev/null +++ b/.github/workflows/tag-release.yml @@ -0,0 +1,28 @@ +name: Release + +on: + push: + tags: + - '**' + +env: + IMAGE_BASE_NAME: dataquest/dspace-angular + +jobs: + retag-FE-image: + runs-on: ubuntu-latest + steps: + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_ACCESS_TOKEN }} + + - name: retag image + run: | + docker pull ${{ env.IMAGE_BASE_NAME }}:${{ github.sha }} + docker tag ${{ env.IMAGE_BASE_NAME }}:${{ github.sha }} ${{ env.IMAGE_BASE_NAME }}:${{ github.ref_name }} + + - name: push image + run: docker push ${{ env.IMAGE_BASE_NAME }}:${{ github.ref_name }} + diff --git a/.github/workflows/trigger-builds.yml b/.github/workflows/trigger-builds.yml new file mode 100644 index 00000000000..1b5ce0b4da8 --- /dev/null +++ b/.github/workflows/trigger-builds.yml @@ -0,0 +1,29 @@ +name: Trigger Build for All Customer Branches + +on: + schedule: + - cron: "0 */4 * * *" # Runs every 4 hours + workflow_dispatch: # Allows manual triggering + +permissions: + actions: write # Grants permission to trigger workflows + contents: read # Access to repository contents + +jobs: + trigger-builds: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 # Ensures the repository is available + + - name: Get and Trigger Customer Branch Builds + run: | + # Authenticate with GitHub CLI using the token + echo ${{ secrets.GITHUB_TOKEN }} | gh auth login --with-token + + git fetch --prune origin # Ensure remote refs are fetched + BRANCHES=$(git ls-remote --heads origin | awk -F'/' '{print $3"/"$4}' | grep '^customer/') + for branch in $(echo "$BRANCHES" | sed -e 's/[\[\]"]//g' -e 's/,/\n/g'); do + echo "Triggering build for branch: $branch" + gh workflow run build.yml --ref $branch + done diff --git a/.github/workflows/trigger-ui-tests.yml b/.github/workflows/trigger-ui-tests.yml new file mode 100644 index 00000000000..f0d53a564b8 --- /dev/null +++ b/.github/workflows/trigger-ui-tests.yml @@ -0,0 +1,37 @@ +name: Trigger UI Tests for All Customer Branches + +on: + workflow_dispatch: + workflow_call: + schedule: + - cron: '0 5 * * *' + +permissions: + actions: write # Grants permission to trigger workflows + contents: read # Access to repository contents + +jobs: + trigger-builds: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Get and Trigger Customer Branch UI Tests + run: | + # Authenticate with GitHub CLI using the token + echo ${{ secrets.GITHUB_TOKEN }} | gh auth login --with-token + + git fetch --prune origin + BRANCHES=$(git ls-remote --heads origin | awk -F'/' '{print $3"/"$4}' | grep '^customer/') + SKIP_BRANCHES=("customer/uk" "customer/mendelu-v7" "customer/palo-docker" "customer/palo-docker-rebase" "customer/palo-docker-typo-fix") + + for branch in $(echo "$BRANCHES" | sed -e 's/[\[\]"]//g' -e 's/,/\n/g'); do + if [[ " ${SKIP_BRANCHES[@]} " =~ " ${branch} " ]]; then + echo "Skipping branch $branch" + continue + fi + + echo "Triggering UI tests for branch: $branch" + gh workflow run playwright-tests.yml --ref $branch + done \ No newline at end of file diff --git a/.gitignore b/.gitignore index 026110f222f..bdab34cb367 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.angular/cache /__build__ /__server_build__ /node_modules @@ -36,3 +37,13 @@ package-lock.json .env /nbproject/ + +junit.xml + +/src/mirador-viewer/config.local.js + +# import data python module +python_data_import/debug.log.txt +python_data_import/logs.txt +python_data_import/date.txt +*/__pycache__/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000000..16fbb4e8d5d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "python_data_import/lib"] + path = python_data_import/lib + url = https://github.com/dataquest-dev/dspace-blackbox-testing.git diff --git a/Acknowledgment-ReadMe.md b/Acknowledgment-ReadMe.md new file mode 100644 index 00000000000..b2caaf86282 --- /dev/null +++ b/Acknowledgment-ReadMe.md @@ -0,0 +1,13 @@ +

+ EOSC CZ Logo +

+ +--- + +This project output was developed with financial contributions from the [EOSC CZ](https://www.eosc.cz/projekty/narodni-podpora-pro-eosc) initiative through the project **National Repository Platform for Research Data** (CZ.02.01.01/00/23_014/0008787), funded by the Programme Johannes Amos Comenius (P JAC) of the Ministry of Education, Youth and Sports of the Czech Republic (MEYS). + +--- + +

+ EU and MŠMT Logos +

diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000000..4e732302f4a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# How to Contribute + +DSpace is a community built and supported project. We do not have a centralized development or support team, but have a dedicated group of volunteers who help us improve the software, documentation, resources, etc. + +* [Contribute new code via a Pull Request](#contribute-new-code-via-a-pull-request) +* [Contribute documentation](#contribute-documentation) +* [Help others on mailing lists or Slack](#help-others-on-mailing-lists-or-slack) +* [Join a working or interest group](#join-a-working-or-interest-group) + +## Contribute new code via a Pull Request + +We accept [GitHub Pull Requests (PRs)](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) at any time from anyone. +Contributors to each release are recognized in our [Release Notes](https://wiki.lyrasis.org/display/DSDOC7x/Release+Notes). + +Code Contribution Checklist +- [ ] PRs _should_ be smaller in size (ideally less than 1,000 lines of code, not including comments & tests) +- [ ] PRs **must** pass [ESLint](https://eslint.org/) validation using `yarn lint` +- [ ] PRs **must** not introduce circular dependencies (verified via `yarn check-circ-deps`) +- [ ] PRs **must** include [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. Large or complex private methods should also have TypeDoc. +- [ ] PRs **must** pass all automated pecs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide). +- [ ] If a PR includes new libraries/dependencies (in `package.json`), then their software licenses **must** align with the [DSpace BSD License](https://github.com/DSpace/dspace-angular/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation. +- [ ] Basic technical documentation _should_ be provided for any new features or configuration, either in the PR itself or in the DSpace Wiki documentation. +- [ ] If a PR fixes an issue ticket, please [link them together](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue). + +Additional details on the code contribution process can be found in our [Code Contribution Guidelines](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines) + +## Contribute documentation + +DSpace Documentation is a collaborative effort in a shared Wiki. The latest documentation is at https://wiki.lyrasis.org/display/DSDOC7x + +If you find areas of the DSpace Documentation which you wish to improve, please request a Wiki account by emailing wikihelp@lyrasis.org. +Once you have an account setup, contact @tdonohue (via [Slack](https://wiki.lyrasis.org/display/DSPACE/Slack) or email) for access to edit our Documentation. + +## Help others on mailing lists or Slack + +DSpace has our own [Slack](https://wiki.lyrasis.org/display/DSPACE/Slack) community and [Mailing Lists](https://wiki.lyrasis.org/display/DSPACE/Mailing+Lists) where discussions take place and questions are answered. +Anyone is welcome to join and help others. We just ask you to follow our [Code of Conduct](https://www.lyrasis.org/about/Pages/Code-of-Conduct.aspx) (adopted via LYRASIS). + +## Join a working or interest group + +Most of the work in building/improving DSpace comes via [Working Groups](https://wiki.lyrasis.org/display/DSPACE/DSpace+Working+Groups) or [Interest Groups](https://wiki.lyrasis.org/display/DSPACE/DSpace+Interest+Groups). + +All working/interest groups are open to anyone to join and participate. A few key groups to be aware of include: + +* [DSpace 7 Working Group](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+Working+Group) - This is the main (mostly volunteer) development team. We meet weekly to review our current development [project board](https://github.com/orgs/DSpace/projects), assigning tickets and/or PRs. +* [DSpace Community Advisory Team (DCAT)](https://wiki.lyrasis.org/display/cmtygp/DSpace+Community+Advisory+Team) - This is an interest group for repository managers/administrators. We meet monthly to discuss DSpace, share tips & provide feedback back to developers. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 2d989711129..e1e72cbf43f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,12 @@ # This image will be published as dspace/dspace-angular # See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details -FROM node:14-alpine +FROM docker.io/node:18-alpine + +# Ensure Python and other build tools are available +# These are needed to install some node modules, especially on linux/arm64 +RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/* + WORKDIR /app ADD . /app/ EXPOSE 4000 @@ -9,4 +14,19 @@ EXPOSE 4000 # We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com # See, for example https://github.com/yarnpkg/yarn/issues/5540 RUN yarn install --network-timeout 300000 -CMD yarn run start:dev + +# When running in dev mode, 4GB of memory is required to build & launch the app. +# This default setting can be overridden as needed in your shell, via an env file or in docker-compose. +# See Docker environment var precedence: https://docs.docker.com/compose/environment-variables/envvars-precedence/ +ENV NODE_OPTIONS="--max_old_space_size=4096" + +# On startup, run in DEVELOPMENT mode (this defaults to live reloading enabled, etc). +# Listen / accept connections from all IP addresses. +# NOTE: At this time it is only possible to run Docker container in Production mode +# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485 +ENV NODE_ENV=development +RUN apk add tzdata +RUN yarn build:prod +RUN npm install pm2 -g +CMD /bin/sh -c "pm2-runtime start docker/dspace-ui.json > /dev/null 2> /dev/null" + diff --git a/Dockerfile.dist b/Dockerfile.dist new file mode 100644 index 00000000000..4c1fc659f4d --- /dev/null +++ b/Dockerfile.dist @@ -0,0 +1,34 @@ +# This image will be published as dspace/dspace-angular:$DSPACE_VERSION-dist +# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details + +# Test build: +# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist . + +FROM docker.io/node:18-alpine AS build + +# Ensure Python and other build tools are available +# These are needed to install some node modules, especially on linux/arm64 +RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/* + +WORKDIR /app +COPY package.json yarn.lock ./ +RUN yarn install --network-timeout 300000 + +ADD . /app/ + +# Set memory limit for build process - Angular builds require more memory +ENV NODE_OPTIONS="--max_old_space_size=4096" +RUN yarn build:prod + +FROM node:18-alpine +RUN npm install --global pm2 + +COPY --chown=node:node --from=build /app/dist /app/dist +COPY --chown=node:node config /app/config +COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json + +WORKDIR /app +USER node +ENV NODE_ENV=production +EXPOSE 4000 +CMD ["pm2-runtime", "start", "dspace-ui.json", "--json"] diff --git a/README-dtq.md b/README-dtq.md new file mode 100644 index 00000000000..5115d7b2901 --- /dev/null +++ b/README-dtq.md @@ -0,0 +1,535 @@ +[![Build Status](https://github.com/DSpace/dspace-angular/workflows/Build/badge.svg?branch=main)](https://github.com/DSpace/dspace-angular/actions?query=workflow%3ABuild) [![Coverage Status](https://codecov.io/gh/DSpace/dspace-angular/branch/main/graph/badge.svg)](https://codecov.io/gh/DSpace/dspace-angular) [![Universal Angular](https://img.shields.io/badge/universal-angular2-brightgreen.svg?style=flat)](https://github.com/angular/universal) + +dspace-angular +============== + +> The DSpace User Interface built on [Angular](https://angular.io/), written in [TypeScript](https://www.typescriptlang.org/) and using [Angular Universal](https://angular.io/guide/universal). + +Overview +-------- + +DSpace open source software is a turnkey repository application used by more than +2,000 organizations and institutions worldwide to provide durable access to digital resources. +For more information, visit http://www.dspace.org/ + +DSpace consists of both a Java-based backend and an Angular-based frontend. + +* Backend (https://github.com/DSpace/DSpace/) provides a REST API, along with other machine-based interfaces (e.g. OAI-PMH, SWORD, etc) + * The REST Contract is at https://github.com/DSpace/RestContract +* Frontend (this codebase) is the User Interface built on the REST API + +Downloads +--------- + +* Backend (REST API): https://github.com/DSpace/DSpace/releases +* Frontend (User Interface): https://github.com/DSpace/dspace-angular/releases + + +## Documentation / Installation + +Documentation for each release may be viewed online or downloaded via our [Documentation Wiki](https://wiki.lyrasis.org/display/DSDOC/). + +The latest DSpace Installation instructions are available at: +https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace + +Quick start +----------- + +**Ensure you're running [Node](https://nodejs.org) `v12.x`, `v14.x` or `v16.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`** + +```bash +# clone the repo +git clone https://github.com/DSpace/dspace-angular.git + +# change directory to our repo +cd dspace-angular + +# install the local dependencies +yarn install + +# start the server +yarn start +``` + +Then go to [http://localhost:4000](http://localhost:4000) in your browser + +Not sure where to start? watch the training videos linked in the [Introduction to the technology](#introduction-to-the-technology) section below. + +Table of Contents +----------------- + +- [Introduction to the technology](#introduction-to-the-technology) +- [Requirements](#requirements) +- [Installing](#installing) + - [Configuring](#configuring) +- [Running the app](#running-the-app) + - [Running in production mode](#running-in-production-mode) + - [Deploy](#deploy) + - [Running the application with Docker](#running-the-application-with-docker) +- [Cleaning](#cleaning) +- [Testing](#testing) + - [Test a Pull Request](#test-a-pull-request) + - [Unit Tests](#unit-tests) + - [E2E Tests](#e2e-tests) + - [Writing E2E Tests](#writing-e2e-tests) +- [Documentation](#documentation) +- [Other commands](#other-commands) +- [Recommended Editors/IDEs](#recommended-editorsides) +- [Collaborating](#collaborating) +- [File Structure](#file-structure) +- [Managing Dependencies (via yarn)](#managing-dependencies-via-yarn) +- [Frequently asked questions](#frequently-asked-questions) +- [License](#license) + +Introduction to the technology +------------------------------ + +You can find more information on the technologies used in this project (Angular.io, Angular CLI, Typescript, Angular Universal, RxJS, etc) on the [LYRASIS wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+UI+Technology+Stack) + +Requirements +------------ + +- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com) +- Ensure you're running node `v12.x`, `v14.x` or `v16.x` and yarn == `v1.x` + +If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS. + +Installing +---------- + +- `yarn install` to install the local dependencies + +### Configuring + +Default configuration file is located in `config/` folder. + +To override the default configuration values, create local files that override the parameters you need to change. You can use `config.example.yml` as a starting point. + +- Create a new `config.(dev or development).yml` file in `config/` for a `development` environment; +- Create a new `config.(prod or production).yml` file in `config/` for a `production` environment; + +The settings can also be overwritten using an environment file or environment variables. + +This file should be called `.env` and be placed in the project root. + +The following non-convention settings: + +```bash +DSPACE_HOST # The host name of the angular application +DSPACE_PORT # The port number of the angular application +DSPACE_NAMESPACE # The namespace of the angular application +DSPACE_SSL # Whether the angular application uses SSL [true/false] +``` + +All other settings can be set using the following convention for naming the environment variables: + +1. replace all `.` with `_` +2. convert all characters to upper case +3. prefix with `DSPACE_` + +e.g. + +```bash +# The host name of the REST application +rest.host => DSPACE_REST_HOST + +# The port number of the REST application +rest.port => DSPACE_REST_PORT + +# The namespace of the REST application +rest.nameSpace => DSPACE_REST_NAMESPACE + +# Whether the angular REST uses SSL [true/false] +rest.ssl => DSPACE_REST_SSL + +cache.msToLive.default => DSPACE_CACHE_MSTOLIVE_DEFAULT +auth.ui.timeUntilIdle => DSPACE_AUTH_UI_TIMEUNTILIDLE +``` + +The equavelant to the non-conventional legacy settings: + +```bash +DSPACE_UI_HOST => DSPACE_HOST +DSPACE_UI_PORT => DSPACE_PORT +DSPACE_UI_NAMESPACE => DSPACE_NAMESPACE +DSPACE_UI_SSL => DSPACE_SSL +``` + +The same settings can also be overwritten by setting system environment variables instead, E.g.: +```bash +export DSPACE_HOST=api7.dspace.org +export DSPACE_UI_PORT=4200 +``` + +The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides external config set by `DSPACE_APP_CONFIG_PATH` overrides **`config.(prod or dev).yml`** + +These configuration sources are collected **at run time**, and written to `dist/browser/assets/config.json` for production and `src/app/assets/config.json` for development. + +The configuration file can be externalized by using environment variable `DSPACE_APP_CONFIG_PATH`. + +#### Using environment variables in code +To use environment variables in a UI component, use: + +```typescript +import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface'; +... +constructor(@Inject(APP_CONFIG) private appConfig: AppConfig) {} +... +``` + +or + +```typescript +import { environment } from '../environment.ts'; +``` + + +Running the app +--------------- + +After you have installed all dependencies you can now run the app. Run `yarn run start:dev` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:4000`. + +### Running in production mode + +When building for production we're using Ahead of Time (AoT) compilation. With AoT, the browser downloads a pre-compiled version of the application, so it can render the application immediately, without waiting to compile the app first. The compiler is roughly half the size of Angular itself, so omitting it dramatically reduces the application payload. + +To build the app for production and start the server run: + +```bash +yarn start +``` +This will run the application in an instance of the Express server, which is included. + +If you only want to build for production, without starting, run: + +```bash +yarn run build:prod +``` +This will build the application and put the result in the `dist` folder. You can copy this folder to wherever you need it for your application server. If you will be using the built-in Express server, you'll also need a copy of the `node_modules` folder tucked inside your copy of `dist`. + + +### Running the application with Docker +NOTE: At this time, we do not have production-ready Docker images for DSpace. +That said, we do have quick-start Docker Compose scripts for development or testing purposes. + +See [Docker Runtime Options](docker/README.md) + + +Cleaning +-------- + +```bash +# clean everything, including node_modules. You'll need to run yarn install again afterwards. +yarn run clean + +# clean files generated by the production build (.ngfactory files, css files, etc) +yarn run clean:prod + +# cleans the distribution directory +yarn run clean:dist +``` + + +Testing +------- + +### Test a Pull Request + +If you would like to contribute by testing a Pull Request (PR), here's how to do so. Keep in mind, you **do not need to have a DSpace backend / REST API installed locally to test a PR**. By default, the dspace-angular project points at our demo REST API + +1. Pull down the branch that the Pull Request was built from. Easy instructions for doing so can be found on the Pull Request itself. + * Next to the "Merge" button, you'll see a link that says "command line instructions". + * Click it, and follow "Step 1" of those instructions to checkout the pull down the PR branch. +2. `yarn run clean` (This resets your local dependencies to ensure you are up-to-date with this PR) +3. `yarn install` (Updates your local dependencies to those in the PR) +4. `yarn start` (Rebuilds the project, and deploys to localhost:4000, by default) +5. At this point, the code from the PR will be deployed to http://localhost:4000. Test it out, and ensure that it does what is described in the PR (or fixes the bug described in the ticket linked to the PR). + +Once you have tested the Pull Request, please add a comment and/or approval to the PR to let us know whether you found it to be successful (or not). Thanks! + + +### Unit Tests + +Unit tests use the [Jasmine test framework](https://jasmine.github.io/), and are run via [Karma](https://karma-runner.github.io/). + +You can find the Karma configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test environment you need to edit the `./karma.conf.js`. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run: `yarn run coverage`. + +The default browser is Google Chrome. + +Place your tests in the same location of the application source code files that they test, e.g. ending with `*.component.spec.ts` + +and run: `yarn test` + +If you run into odd test errors, see the Angular guide to debugging tests: https://angular.io/guide/test-debugging + +Run single unit test + +Edit `src/test-dtq.ts` file to load only the file for testing. +### E2E Tests + +E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Configuration for cypress can be found in the `cypress.json` file in the root directory. + +The test files can be found in the `./cypress/integration/` folder. + +Before you can run e2e tests, two things are required: +1. You MUST have a running backend (i.e. REST API). By default, the e2e tests look for this at http://localhost:8080/server/ or whatever `rest` backend is defined in your `config.prod.yml` or `config.yml`. You may override this using env variables, see [Configuring](#configuring). +2. Your backend MUST include our Entities Test Data set. Some tests run against a (currently hardcoded) Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set. The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data + +Run `ng e2e` to kick off the tests. This will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results. + +#### Writing E2E Tests + +All E2E tests must be created under the `./cypress/integration/` folder, and must end in `.spec.ts`. Subfolders are allowed. + +* The easiest way to start creating new tests is by running `ng e2e`. This builds the app and brings up Cypress. +* From here, if you are editing an existing test file, you can either open it in your IDE or run it first to see what it already does. +* To create a new test file, click `+ New Spec File`. Choose a meaningful name ending in `spec.ts` (Please make sure it ends in `.ts` so that it's a Typescript file, and not plain Javascript) +* Start small. Add a basic `describe` and `it` which just [cy.visit](https://docs.cypress.io/api/commands/visit) the page you want to test. For example: + ``` + describe('Community/Collection Browse Page', () => { + it('should exist as a page', () => { + cy.visit('/community-list'); + }); + }); + ``` +* Run your test file from the Cypress window. This starts the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) in a new browser window. +* In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_. +* From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page. + * Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector + * Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc. + * Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions. +* Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly. +* Cypress also has a great guide on [writing your first test](https://on.cypress.io/writing-first-test) with much more info. Keep in mind, while the examples in the Cypress docs often involve Javascript files (.js), the same examples will work in our Typescript (.ts) e2e tests. + +_Hint: Creating e2e tests is easiest in an IDE (like Visual Studio), as it can help prompt/autocomplete your Cypress commands._ + +More Information: [docs.cypress.io](https://docs.cypress.io/) has great guides & documentation helping you learn more about writing/debugging e2e tests in Cypress. + +### Learning how to build tests + +See our [DSpace Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide) for more hints/tips. + +Documentation +-------------- + +Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC7x/ + +Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of htis codebase. + +### Building code documentation + +To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts information from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments. + +Run:`yarn run docs` to produce the documentation that will be available in the 'doc' folder. + +Other commands +-------------- + +There are many more commands in the `scripts` section of `package.json`. Most of these are executed by one of the commands mentioned above. + +A command with a name that starts with `pre` or `post` will be executed automatically before or after the script with the matching name. e.g. if you type `yarn run start` the `prestart` script will run first, then the `start` script will trigger. + +Recommended Editors/IDEs +------------------------ + +To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've had good experiences using these editors: + +- Free + - [Visual Studio Code](https://code.visualstudio.com/) + - [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome) +- Paid + - [Webstorm](https://www.jetbrains.com/webstorm/download/) or [IntelliJ IDEA Ultimate](https://www.jetbrains.com/idea/) + - [Sublime Text](http://www.sublimetext.com/3) + - [Typescript-Sublime-Plugin](https://github.com/Microsoft/Typescript-Sublime-plugin#installation) + +Collaborating +------------- + +See [the guide on the wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development#DSpace7-AngularUIDevelopment-Howtocontribute) + +File Structure +-------------- + +``` +dspace-angular +├── config * +│ └── config.yml * Default app config +├── cypress * Folder for Cypress (https://cypress.io/) / e2e tests +│ ├── downloads * +│ ├── fixtures * Folder for e2e/integration test files +│ ├── integration * Folder for any fixtures needed by e2e tests +│ ├── plugins * Folder for Cypress plugins (if any) +│ ├── support * Folder for global e2e test actions/commands (run for all tests) +│ └── tsconfig.json * TypeScript configuration file for e2e tests +├── docker * See docker/README.md for details +│ ├── cli.assetstore.yml * +│ ├── cli.ingest.yml * +│ ├── cli.yml * +│ ├── db.entities.yml * +│ ├── docker-compose-ci.yml * +│ ├── docker-compose-rest.yml * +│ ├── docker-compose.yml * +│ └── README.md * +├── docs * Folder for documentation +│ └── Configuration.md * Configuration documentation +├── scripts * +│ ├── merge-i18n-files.ts * +│ ├── serve.ts * +│ ├── sync-i18n-files.ts * +│ ├── test-rest.ts * +│ └── webpack.js * +├── src * The source of the application +│ ├── app * The source code of the application, subdivided by module/page. +│ ├── assets * Folder for static resources +│ │ ├── fonts * Folder for fonts +│ │ ├── i18n * Folder for i18n translations +│ │ └── images * Folder for images +│ ├── backend * Folder containing a mock of the REST API, hosted by the express server +│ ├── config * +│ ├── environments * +│ │ ├── environment.production.ts * Production configuration files +│ │ ├── environment.test.ts * Test configuration files +│ │ └── environment.ts * Default (development) configuration files +│ ├── mirador-viewer * +│ ├── modules * +│ ├── ngx-translate-loaders * +│ ├── styles * Folder containing global styles +│ ├── themes * Folder containing available themes +│ │ ├── custom * Template folder for creating a custom theme +│ │ └── dspace * Default 'dspace' theme +│ ├── index.csr.html * The index file for client side rendering fallback +│ ├── index.html * The index file +│ ├── main.browser.ts * The bootstrap file for the client +│ ├── main.server.ts * The express (http://expressjs.com/) config and bootstrap file for the server +│ ├── polyfills.ts * +│ ├── robots.txt * The robots.txt file +│ ├── test.ts * +│ └── typings.d.ts * +├── webpack * +│ ├── helpers.ts * Webpack helpers +│ ├── webpack.browser.ts * Webpack (https://webpack.github.io/) config for browser build +│ ├── webpack.common.ts * Webpack (https://webpack.github.io/) common build config +│ ├── webpack.mirador.config.ts * Webpack (https://webpack.github.io/) config for mirador config build +│ ├── webpack.prod.ts * Webpack (https://webpack.github.io/) config for prod build +│ └── webpack.test.ts * Webpack (https://webpack.github.io/) config for test build +├── angular.json * Angular CLI (https://angular.io/cli) configuration +├── cypress.json * Cypress Test (https://www.cypress.io/) configuration +├── Dockerfile * +├── karma.conf.js * Karma configuration file for Unit Test +├── LICENSE * +├── LICENSES_THIRD_PARTY * +├── nodemon.json * Nodemon (https://nodemon.io/) configuration +├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc. +├── postcss.config.js * PostCSS (http://postcss.org/) configuration +├── README.md * This document +├── SECURITY.md * +├── server.ts * Angular Universal Node.js Express server +├── tsconfig.app.json * TypeScript config for browser (app) +├── tsconfig.json * TypeScript common config +├── tsconfig.server.json * TypeScript config for server +├── tsconfig.spec.json * TypeScript config for tests +├── tsconfig.ts-node.json * TypeScript config for using ts-node directly +├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration +├── typedoc.json * TYPEDOC configuration +└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock) +``` + +Managing Dependencies (via yarn) +------------- + +This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the exact same dependency versions are used every time you install it. + +* `yarn` creates a [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via yarn. +* **Adding new dependencies**: To install/add a new dependency (third party library), use [`yarn add`](https://yarnpkg.com/en/docs/cli/add). For example: `yarn add some-lib`. + * If you are adding a new build tool dependency (to `devDependencies`), use `yarn add some-lib --dev` +* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`yarn upgrade`](https://yarnpkg.com/en/docs/cli/upgrade). For example: `yarn upgrade some-lib` or `yarn upgrade some-lib@version` +* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`yarn remove`](https://yarnpkg.com/en/docs/cli/remove) to remove it. + +As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.* + +### Adding Typings for libraries + +If the library does not include typings, you can install them using yarn: + +```bash +yarn add d3 +yarn add @types/d3 --dev +``` + +If the library doesn't have typings available at `@types/`, you can still use it by manually adding typings for it: + +1. In `src/typings.d.ts`, add the following code: + + ```typescript + declare module 'typeless-package'; + ``` + +2. Then, in the component or file that uses the library, add the following code: + + ```typescript + import * as typelessPackage from 'typeless-package'; + typelessPackage.method(); + ``` + +Done. Note: you might need or find useful to define more typings for the library that you're trying to use. + +If you're importing a module that uses CommonJS you need to import as + +```typescript +import * as _ from 'lodash'; +``` + +Frequently asked questions +-------------------------- + +- Why is my service, aka provider, is not injecting a parameter correctly? + - Please use `@Injectable()` for your service for typescript to correctly attach the metadata +- Where do I write my tests? + - You can write your tests next to your component files. e.g. for `src/app/home/home.component.ts` call it `src/app/home/home.component.spec.ts` +- How do I start the app when I get `EACCES` and `EADDRINUSE` errors? + - The `EADDRINUSE` error means the port `4000` is currently being used and `EACCES` is lack of permission to build files to `./dist/` +- What are the naming conventions for Angular? + - See [the official angular style guide](https://angular.io/styleguide) +- Why is the size of my app larger in development? + - The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the intrest of build speed. +- node-pre-gyp ERR in yarn install (Windows) + - install Python x86 version between 2.5 and 3.0 on windows. See [this issue](https://github.com/AngularClass/angular2-webpack-starter/issues/626) +- How do I handle merge conflicts in yarn.lock? + - first check out the yarn.lock file from the branch you're merging in to yours: e.g. `git checkout --theirs yarn.lock` + - now run `yarn install` again. Yarn will create a new lockfile that contains both sets of changes. + - then run `git add yarn.lock` to stage the lockfile for commit + - and `git commit` to conclude the merge + +Getting Help +------------ + +DSpace provides public mailing lists where you can post questions or raise topics for discussion. +We welcome everyone to participate in these lists: + +* [dspace-community@googlegroups.com](https://groups.google.com/d/forum/dspace-community) : General discussion about DSpace platform, announcements, sharing of best practices +* [dspace-tech@googlegroups.com](https://groups.google.com/d/forum/dspace-tech) : Technical support mailing list. See also our guide for [How to troubleshoot an error](https://wiki.lyrasis.org/display/DSPACE/Troubleshoot+an+error). +* [dspace-devel@googlegroups.com](https://groups.google.com/d/forum/dspace-devel) : Developers / Development mailing list + +Great Q&A is also available under the [DSpace tag on Stackoverflow](http://stackoverflow.com/questions/tagged/dspace) + +Additional support options are at https://wiki.lyrasis.org/display/DSPACE/Support + +DSpace also has an active service provider network. If you'd rather hire a service provider to +install, upgrade, customize or host DSpace, then we recommend getting in touch with one of our +[Registered Service Providers](http://www.dspace.org/service-providers). + + +Issue Tracker +------------- + +DSpace uses GitHub to track issues: +* Backend (REST API) issues: https://github.com/DSpace/DSpace/issues +* Frontend (User Interface) issues: https://github.com/DSpace/dspace-angular/issues + +License +------- +DSpace source code is freely available under a standard [BSD 3-Clause license](https://opensource.org/licenses/BSD-3-Clause). +The full license is available in the [LICENSE](LICENSE) file or online at http://www.dspace.org/license/ + +DSpace uses third-party libraries which may be distributed under different licenses. Those licenses are listed +in the [LICENSES_THIRD_PARTY](LICENSES_THIRD_PARTY) file. diff --git a/README.md b/README.md index ca27ce9ebef..0be2f23c29d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://github.com/DSpace/dspace-angular/workflows/Build/badge.svg?branch=main)](https://github.com/DSpace/dspace-angular/actions?query=workflow%3ABuild) [![Coverage Status](https://codecov.io/gh/DSpace/dspace-angular/branch/main/graph/badge.svg)](https://codecov.io/gh/DSpace/dspace-angular) [![Universal Angular](https://img.shields.io/badge/universal-angular2-brightgreen.svg?style=flat)](https://github.com/angular/universal) +[![Build](https://github.com/dataquest-dev/dspace-angular/actions/workflows/build.yml/badge.svg)](https://github.com/dataquest-dev/dspace-angular/actions/workflows/build.yml) [![codecov](https://codecov.io/gh/dataquest-dev/dspace-angular/branch/dtq-dev/graph/badge.svg?token=DQ7QIZN8S6)](https://codecov.io/gh/dataquest-dev/dspace-angular) [![Universal Angular](https://img.shields.io/badge/universal-angular2-brightgreen.svg?style=flat)](https://github.com/angular/universal) dspace-angular ============== @@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace Quick start ----------- -**Ensure you're running [Node](https://nodejs.org) `v12.x`, `v14.x` or `v16.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`** +**Ensure you're running [Node](https://nodejs.org) `v16.x` or `v18.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`** ```bash # clone the repo @@ -90,7 +90,7 @@ Requirements ------------ - [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com) -- Ensure you're running node `v12.x`, `v14.x` or `v16.x` and yarn == `v1.x` +- Ensure you're running node `v16.x` or `v18.x` and yarn == `v1.x` If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS. @@ -157,8 +157,8 @@ DSPACE_UI_SSL => DSPACE_SSL The same settings can also be overwritten by setting system environment variables instead, E.g.: ```bash -export DSPACE_HOST=api7.dspace.org -export DSPACE_UI_PORT=4200 +export DSPACE_HOST=demo.dspace.org +export DSPACE_UI_PORT=4000 ``` The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides external config set by `DSPACE_APP_CONFIG_PATH` overrides **`config.(prod or dev).yml`** @@ -179,7 +179,7 @@ If needing to update default configurations values for production, update local - Update `environment.production.ts` file in `src/environment/` for a `production` environment; -The environment object is provided for use as import in code and is extended with he runtime configuration on bootstrap of the application. +The environment object is provided for use as import in code and is extended with the runtime configuration on bootstrap of the application. > Take caution moving runtime configs into the buildtime configuration. They will be overwritten by what is defined in the runtime config on bootstrap. @@ -288,7 +288,7 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con The test files can be found in the `./cypress/integration/` folder. Before you can run e2e tests, two things are REQUIRED: -1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo REST API (https://api7.dspace.org/server/), as that server is uncontrolled and may have content added/removed at any time. +1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo/sandbox REST API (https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/), as those sites may have content added/removed at any time. * After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend. * If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example: ``` @@ -330,8 +330,11 @@ All E2E tests must be created under the `./cypress/integration/` folder, and mus * In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_. * From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page. * Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector + * It's generally best not to rely on attributes like `class` and `id` in tests, as those are likely to change later on. Instead, you can add a `data-test` attribute to makes it clear that it's required for a test. * Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc. - * Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions. + * When running with server-side rendering enabled, the client first receives HTML without the JS; only once the page is rendered client-side do some elements (e.g. a button that toggles a Bootstrap dropdown) become fully interactive. This can trip up Cypress in some cases as it may try to `click` or `type` in an element that's not fully loaded yet, causing tests to fail. + * To work around this issue, define the attributes you use for Cypress selectors as `[attr.data-test]="'button' | ngBrowserOnly"`. This will only show the attribute in CSR HTML, forcing Cypress to wait until CSR is complete before interacting with the element. + * Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions. * Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly. * Cypress also has a great guide on [writing your first test](https://on.cypress.io/writing-first-test) with much more info. Keep in mind, while the examples in the Cypress docs often involve Javascript files (.js), the same examples will work in our Typescript (.ts) e2e tests. @@ -348,7 +351,7 @@ Documentation Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC7x/ -Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of htis codebase. +Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of this codebase. ### Building code documentation @@ -376,10 +379,10 @@ To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've - [Sublime Text](http://www.sublimetext.com/3) - [Typescript-Sublime-Plugin](https://github.com/Microsoft/Typescript-Sublime-plugin#installation) -Collaborating +Contributing ------------- -See [the guide on the wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development#DSpace7-AngularUIDevelopment-Howtocontribute) +See [Contributing documentation](CONTRIBUTING.md) File Structure -------------- @@ -410,8 +413,7 @@ dspace-angular │ ├── merge-i18n-files.ts * │ ├── serve.ts * │ ├── sync-i18n-files.ts * -│ ├── test-rest.ts * -│ └── webpack.js * +│ └── test-rest.ts * ├── src * The source of the application │ ├── app * The source code of the application, subdivided by module/page. │ ├── assets * Folder for static resources @@ -567,3 +569,8 @@ The full license is available in the [LICENSE](LICENSE) file or online at http:/ DSpace uses third-party libraries which may be distributed under different licenses. Those licenses are listed in the [LICENSES_THIRD_PARTY](LICENSES_THIRD_PARTY) file. + +Additional tools +---------------- + +This project is tested with BrowserStack. diff --git a/angular.json b/angular.json index a0a4cd8ea15..a57b7582109 100644 --- a/angular.json +++ b/angular.json @@ -17,7 +17,6 @@ "build": { "builder": "@angular-builders/custom-webpack:browser", "options": { - "extractCss": true, "preserveSymlinks": true, "customWebpackConfig": { "path": "./webpack/webpack.browser.ts", @@ -26,14 +25,11 @@ } }, "allowedCommonJsDependencies": [ - "angular2-text-mask", "cerialize", "core-js", "lodash", "jwt-decode", - "url-parse", "uuid", - "webfontloader", "zone.js" ], "outputPath": "dist/browser", @@ -48,6 +44,8 @@ ], "styles": [ "src/styles/startup.scss", + "src/aai/discojuice/discojuice.css", + "node_modules/bootstrap/dist/css/bootstrap.min.css", { "input": "src/styles/base-theme.scss", "inject": false, @@ -64,19 +62,35 @@ "bundleName": "dspace-theme" } ], - "scripts": [] + "scripts": [ + "src/license-selector.js", + "src/license-selector-creation.js", + "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" + ], + "baseHref": "/" }, "configurations": { + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + }, "production": { "fileReplacements": [ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.production.ts" + }, + { + "replace": "src/config/store/devtools.ts", + "with": "src/config/store/devtools.prod.ts" } ], "optimization": true, "outputHashing": "all", - "extractCss": true, "namedChunks": false, "aot": true, "extractLicenses": true, @@ -104,6 +118,9 @@ "port": 4000 }, "configurations": { + "development": { + "browserTarget": "dspace-angular:build:development" + }, "production": { "browserTarget": "dspace-angular:build:production" } @@ -157,19 +174,6 @@ } } }, - "lint": { - "builder": "@angular-devkit/build-angular:tslint", - "options": { - "tsConfig": [ - "tsconfig.app.json", - "tsconfig.spec.json", - "cypress/tsconfig.json" - ], - "exclude": [ - "**/node_modules/**" - ] - } - }, "e2e": { "builder": "@cypress/schematic:cypress", "options": { @@ -197,6 +201,10 @@ "tsConfig": "tsconfig.server.json" }, "configurations": { + "development": { + "sourceMap": true, + "optimization": false + }, "production": { "sourceMap": false, "optimization": true, @@ -204,6 +212,10 @@ { "replace": "src/environments/environment.ts", "with": "src/environments/environment.production.ts" + }, + { + "replace": "src/config/store/devtools.ts", + "with": "src/config/store/devtools.prod.ts" } ] } @@ -253,12 +265,32 @@ "watch": true, "headless": false } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "src/**/*.ts", + "src/**/*.html", + "src/**/*.json5" + ] + } } } } }, - "defaultProject": "dspace-angular", "cli": { - "analytics": false + "analytics": false, + "schematicCollections": [ + "@angular-eslint/schematics" + ] + }, + "schematics": { + "@angular-eslint/schematics:application": { + "setParserOptionsProject": true + }, + "@angular-eslint/schematics:library": { + "setParserOptionsProject": true + } } -} \ No newline at end of file +} diff --git a/build-scripts/import/assets/test_community_collection.xml b/build-scripts/import/assets/test_community_collection.xml new file mode 100644 index 00000000000..ea9580ca595 --- /dev/null +++ b/build-scripts/import/assets/test_community_collection.xml @@ -0,0 +1,19 @@ + + + + Community Name + Descriptive text + Introductory text + Special copyright notice + Sidebar text + + Collection Name + Descriptive text + Introductory text + Special copyright notice + Sidebar text + Special licence + Provenance information + + + \ No newline at end of file diff --git a/build-scripts/import/harvest.bat b/build-scripts/import/harvest.bat new file mode 100644 index 00000000000..647de14f4f7 --- /dev/null +++ b/build-scripts/import/harvest.bat @@ -0,0 +1,13 @@ +IF "%ENVFILE%"=="" set ENVFILE=%cd%/envs/.default + +:: wiki: https://wiki.lyrasis.org/display/DSDOC7x/OAI#OAI-HarvestingfromanotherDSpace +pushd ..\.. +:: import community with collection +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/cli.yml -v build-scripts/import/assets:/assets run --rm dspace-cli structure-builder -f /assets/test_community_collection.xml -o /assets/test_import_output.xml -e dspace.admin.dev@dataquest.sk +:: test connection +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/cli.yml run --rm dspace-cli harvest -g -a http://lindat.mff.cuni.cz/repository/oai/request -i hdl_11234_3430 +:: set up collection for harvesting +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/cli.yml run --rm dspace-cli harvest -s -c 123456789/2 -a http://lindat.mff.cuni.cz/repository/oai/request -i hdl_11234_3430 -m dc -t 1 -e dspace.admin.dev@dataquest.sk +:: start harvesting +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/cli.yml run --rm dspace-cli harvest -r -c 123456789/2 -a http://lindat.mff.cuni.cz/repository/oai/request -i hdl_11234_3430 -m dc -t 1 -e dspace.admin.dev@dataquest.sk +popd diff --git a/build-scripts/import/harvest.sh b/build-scripts/import/harvest.sh new file mode 100644 index 00000000000..15f308907a5 --- /dev/null +++ b/build-scripts/import/harvest.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +if [[ "x$ENVFILE" == "x" ]]; then + export ENVFILE=$(pwd)/../run/envs/.default +fi + +source $ENVFILE + +# wiki: https://wiki.lyrasis.org/display/DSDOC7x/OAI#OAI-HarvestingfromanotherDSpace +pushd ../.. +# import community with collection +docker-compose --env-file $ENVFILE -p dq-d7 -f docker/cli.yml -v $(pwd)/build-scripts/import/assets:/assets run --rm dspace-cli structure-builder -f /assets/test_community_collection.xml -o /assets/test_import_output.xml -e dspace.admin.dev@dataquest.sk +# test connection +docker-compose --env-file $ENVFILE -p dq-d7 -f docker/cli.yml run --rm dspace-cli harvest -g -a http://lindat.mff.cuni.cz/repository/oai/request -i hdl_11234_3430 +# set up collection for harvesting +docker-compose --env-file $ENVFILE -p dq-d7 -f docker/cli.yml run --rm dspace-cli harvest -s -c 123456789/2 -a http://lindat.mff.cuni.cz/repository/oai/request -i hdl_11234_3430 -m dc -t 1 -e dspace.admin.dev@dataquest.sk +# start harvesting +docker-compose --env-file $ENVFILE -p dq-d7 -f docker/cli.yml run --rm dspace-cli harvest -r -c 123456789/2 -a http://lindat.mff.cuni.cz/repository/oai/request -i hdl_11234_3430 -m dc -t 1 -e dspace.admin.dev@dataquest.sk +popd diff --git a/build-scripts/run/.gitignore b/build-scripts/run/.gitignore new file mode 100644 index 00000000000..482e66411b7 --- /dev/null +++ b/build-scripts/run/.gitignore @@ -0,0 +1,3 @@ +!env +!.env +!.env* \ No newline at end of file diff --git a/build-scripts/run/README.md b/build-scripts/run/README.md new file mode 100644 index 00000000000..5bc3d5e07c6 --- /dev/null +++ b/build-scripts/run/README.md @@ -0,0 +1,30 @@ +# Run in docker + +## Locally + +Build local image `dspace-angular`: +``` +cd ../.. +docker build . -t dspace-angular +``` + +Start front-end (local `dspace-angular` image) locally, see `.env.local` +``` +start.frontend.local.bat +``` + +Start backend +``` +start.backend.bat +``` + +## With remote images + +``` +start.bat +``` + + +# Frontend + +./Dockerfile -> `yarn run start:dev` -> ./package.json -> nodemon `yarn run serve` -> ts-node `scripts/serve.ts` -> `ng serve` diff --git a/build-scripts/run/assetstore/77/89/37/77893754617268908529226218097860272513 b/build-scripts/run/assetstore/77/89/37/77893754617268908529226218097860272513 new file mode 100644 index 00000000000..0b5b3cb4b8f --- /dev/null +++ b/build-scripts/run/assetstore/77/89/37/77893754617268908529226218097860272513 @@ -0,0 +1,36 @@ +NOTE: PLACE YOUR OWN LICENSE HERE +This sample license is provided for informational purposes only. + +NON-EXCLUSIVE DISTRIBUTION LICENSE + +By signing and submitting this license, you (the author(s) or copyright +owner) grants to DSpace University (DSU) the non-exclusive right to reproduce, +translate (as defined below), and/or distribute your submission (including +the abstract) worldwide in print and electronic format and in any medium, +including but not limited to audio or video. + +You agree that DSU may, without changing the content, translate the +submission to any medium or format for the purpose of preservation. + +You also agree that DSU may keep more than one copy of this submission for +purposes of security, back-up and preservation. + +You represent that the submission is your original work, and that you have +the right to grant the rights contained in this license. You also represent +that your submission does not, to the best of your knowledge, infringe upon +anyone's copyright. + +If the submission contains material for which you do not hold copyright, +you represent that you have obtained the unrestricted permission of the +copyright owner to grant DSU the rights required by this license, and that +such third-party owned material is clearly identified and acknowledged +within the text or content of the submission. + +IF THE SUBMISSION IS BASED UPON WORK THAT HAS BEEN SPONSORED OR SUPPORTED +BY AN AGENCY OR ORGANIZATION OTHER THAN DSU, YOU REPRESENT THAT YOU HAVE +FULFILLED ANY RIGHT OF REVIEW OR OTHER OBLIGATIONS REQUIRED BY SUCH +CONTRACT OR AGREEMENT. + +DSU will clearly identify your name(s) as the author(s) or owner(s) of the +submission, and will not make any alteration, other than as allowed by this +license, to your submission. diff --git a/build-scripts/run/check.logs.bat b/build-scripts/run/check.logs.bat new file mode 100644 index 00000000000..33c6c111477 --- /dev/null +++ b/build-scripts/run/check.logs.bat @@ -0,0 +1,10 @@ +IF "%ENVFILE%"=="" set ENVFILE=%cd%/envs/.default + +pushd ..\.. +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml logs -f -t +popd + +IF "%1"=="nopause" GOTO No1 + echo %~n0 + pause +:No1 \ No newline at end of file diff --git a/build-scripts/run/envs/.default b/build-scripts/run/envs/.default new file mode 100644 index 00000000000..b6bac7ac647 --- /dev/null +++ b/build-scripts/run/envs/.default @@ -0,0 +1,6 @@ +DSPACE_UI_IMAGE=dataquest/dspace-angular:dspace-7_x +DSPACE_REST_IMAGE=dataquest/dspace:dspace-7_x +DOCKER_OWNER=dataquest +DSPACE_REST_HOST=dev-5.pc +REST_URL=http://dev-5.pc:8080/server +UI_URL=http://dev-5.pc diff --git a/build-scripts/run/envs/.local b/build-scripts/run/envs/.local new file mode 100644 index 00000000000..cfa0874bc35 --- /dev/null +++ b/build-scripts/run/envs/.local @@ -0,0 +1,2 @@ +DSPACE_UI_HOST=0.0.0.0 +DSPACE_UI_IMAGE=dspace-angular diff --git a/build-scripts/run/reindex.sh b/build-scripts/run/reindex.sh new file mode 100755 index 00000000000..f20d5bfb6c4 --- /dev/null +++ b/build-scripts/run/reindex.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +if [[ "x$ENVFILE" == "x" ]]; then + export ENVFILE=$(pwd)/envs/.default +fi + +pushd ../.. +docker-compose --env-file $ENVFILE -p dq-d7 -f docker/cli.yml run --rm dspace-cli oai import -c +docker-compose --env-file $ENVFILE -p dq-d7 -f docker/cli.yml run --rm dspace-cli index-discovery +docker-compose --env-file $ENVFILE -p dq-d7 -f docker/cli.yml run --rm dspace-cli database migrate force +# docker-compose --env-file $ENVFILE -p dq-d7 -f docker/cli.yml run --rm dspace-cli +# docker-compose --env-file $ENVFILE -p dq-d7 -f docker/cli.yml run --rm dspace-cli +# docker-compose --env-file $ENVFILE -p dq-d7 -f docker/cli.yml run --rm dspace-cli +popd diff --git a/build-scripts/run/start.backend.bat b/build-scripts/run/start.backend.bat new file mode 100644 index 00000000000..a79ad8f9310 --- /dev/null +++ b/build-scripts/run/start.backend.bat @@ -0,0 +1,11 @@ +IF "%ENVFILE%"=="" set ENVFILE=%cd%/envs/.default + +pushd ..\.. +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/docker-compose-rest.yml pull dspace dspacesolr dspacedb +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/docker-compose-rest.yml up -d --force-recreate --no-build dspace dspacesolr dspacedb +popd + +IF "%1"=="nopause" GOTO No1 + echo %~n0 + pause +:No1 \ No newline at end of file diff --git a/build-scripts/run/start.bat b/build-scripts/run/start.bat new file mode 100644 index 00000000000..d1c0102fe3d --- /dev/null +++ b/build-scripts/run/start.bat @@ -0,0 +1,20 @@ +REM set DSPACE_REST_HOST=dev-5.pc +REM set REST_URL=http://dev-5.pc:8080/server +REM set UI_URL=http://dev-5.pc/ +set DSPACE_REST_IMAGE=dataquest/dspace:dspace-7_x +set DOCKER_OWNER=dataquest + +IF "%ENVFILE%"=="" set ENVFILE=%cd%/envs/.default + +call start.backend.bat nopause +call start.frontend.bat nopause + +pushd ..\.. +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e dspace.admin.dev@dataquest.sk -f admin -l user -p admin -c en +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/cli.yml run --rm dspace-cli version +popd + +IF "%1"=="nopause" GOTO No1 + echo %~n0 + pause +:No1 \ No newline at end of file diff --git a/build-scripts/run/start.frontend.bat b/build-scripts/run/start.frontend.bat new file mode 100644 index 00000000000..d333430f0e9 --- /dev/null +++ b/build-scripts/run/start.frontend.bat @@ -0,0 +1,14 @@ +IF "%ENVFILE%"=="" set ENVFILE=%cd%/envs/.default + +REM TODO: hardcoded! +docker pull dataquest/dspace-angular:dspace-7_x + +pushd ..\.. +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/docker-compose.yml pull dspace-angular +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/docker-compose.yml up -d --force-recreate --no-build dspace-angular +popd + +IF "%1"=="nopause" GOTO No1 + echo %~n0 + pause +:No1 \ No newline at end of file diff --git a/build-scripts/run/start.frontend.local.bat b/build-scripts/run/start.frontend.local.bat new file mode 100644 index 00000000000..206259ac824 --- /dev/null +++ b/build-scripts/run/start.frontend.local.bat @@ -0,0 +1,8 @@ +IF "%ENVFILE%"=="" set ENVFILE=%cd%/envs/.local + +start.frontend.bat nopause + +IF "%1"=="nopause" GOTO No1 + echo %~n0 + pause +:No1 \ No newline at end of file diff --git a/build-scripts/run/start.sh b/build-scripts/run/start.sh new file mode 100755 index 00000000000..5dfdfc8cd4d --- /dev/null +++ b/build-scripts/run/start.sh @@ -0,0 +1,53 @@ +#!/bin/bash +if [[ "x$ENVFILE" == "x" ]]; then + export ENVFILE=$(pwd)/envs/.default + echo "Using default envfile" +fi + +PROJECT=${1:-unnamed_dspace} + +echo "Using envfile: [$ENVFILE] for project: [$PROJECT]" + +source $ENVFILE + +# docker-compose does not pull those that have `build` section?! +echo "=====" +docker pull $DSPACE_UI_IMAGE + +pushd ../.. +echo "=====" +docker compose --env-file $ENVFILE -f docker/docker-compose.yml -f docker/docker-compose-rest.yml pull +docker compose --env-file $ENVFILE -p $PROJECT -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --no-build +popd + +# Create admin user +# set DOCKER_OWNER to match our image (see cli.yml) +pushd ../.. +echo "=====" +#docker compose --env-file $ENVFILE -p $PROJECT -f docker/matomo-w-db.yml pull +#docker compose --env-file $ENVFILE -p $PROJECT -f docker/matomo-w-db.yml up -d --no-build + +# docker-compose-rest.yml must be last, since it specifies network in more detail. If it is not last, there is "root must be a mapping" error. +if [[ -z "$ADMIN_PASSWORD" ]]; then + echo "ERROR: ADMIN_PASSWORD is required but not set." >&2 + exit 1 +fi +if [[ -z "$USER_PASSWORD" ]]; then + echo "ERROR: USER_PASSWORD is required but not set." >&2 + exit 1 +fi +docker compose --env-file $ENVFILE -p $PROJECT -f docker/docker-compose.yml -f docker/cli.yml -f docker/docker-compose-rest.yml run --rm dspace-cli create-administrator -e dspace.admin.dev@dataquest.sk -f admin -l user -p "${ADMIN_PASSWORD}" -c en -o dataquest +docker compose --env-file $ENVFILE -p $PROJECT -f docker/docker-compose.yml -f docker/cli.yml -f docker/docker-compose-rest.yml run --rm dspace-cli user --add -m dspace.user.dev@dataquest.sk -g meno -s priezvisko -l en -p "${USER_PASSWORD}" -o dataquest +docker compose --env-file $ENVFILE -p $PROJECT -f docker/docker-compose.yml -f docker/cli.yml -f docker/docker-compose-rest.yml run --rm dspace-cli version + +echo "=====" +echo "Logs" +docker compose --env-file $ENVFILE -p $PROJECT -f docker/docker-compose.yml -f docker/docker-compose-rest.yml logs -n 50 || true +popd + +echo "=====" +echo "Copy assetstore" +docker cp assetstore dspace${INSTANCE}:/dspace/ + +echo "=====" +echo "Finished start.sh" diff --git a/build-scripts/run/stop.bat b/build-scripts/run/stop.bat new file mode 100644 index 00000000000..dd9462a0323 --- /dev/null +++ b/build-scripts/run/stop.bat @@ -0,0 +1,7 @@ +IF "%ENVFILE%"=="" set ENVFILE=%cd%/envs/.default + +pushd ..\.. +docker-compose --env-file %ENVFILE% -p dq-d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml down +popd + +pause diff --git a/config/config.example.yml b/config/config.example.yml index 771c7b16530..8b56711c7d2 100644 --- a/config/config.example.yml +++ b/config/config.example.yml @@ -1,8 +1,9 @@ # NOTE: will log all redux actions and transfers in console debug: false -# Angular Universal server settings -# NOTE: these must be 'synced' with the 'dspace.ui.url' setting in your backend's local.cfg. +# Angular User Inteface settings +# NOTE: these settings define where Node.js will start your UI application. Therefore, these +# "ui" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar) ui: ssl: false host: localhost @@ -13,27 +14,124 @@ ui: rateLimiter: windowMs: 60000 # 1 minute max: 500 # limit each IP to 500 requests per windowMs + # Trust X-FORWARDED-* headers from proxies (default = true) + useProxies: true + +# Angular Universal / Server Side Rendering (SSR) settings +universal: + # Whether to tell Angular to inline "critical" styles into the server-side rendered HTML. + # Determining which styles are critical is a relatively expensive operation; this option is + # disabled (false) by default to boost server performance at the expense of loading smoothness. + inlineCriticalCss: false + # Patterns to be run as regexes against the path of the page to check if SSR is allowed. + # If the path match any of the regexes it will be served directly in CSR. + # By default, excludes community and collection browse, global browse, global search, community list, statistics and various administrative tools. + excludePathPatterns: + - pattern: "^/communities/[a-f0-9-]{36}/browse(/.*)?$" + flag: "i" + - pattern: "^/collections/[a-f0-9-]{36}/browse(/.*)?$" + flag: "i" + - pattern: "^/browse/" + - pattern: "^/search$" + - pattern: "^/community-list$" + - pattern: "^/admin/" + - pattern: "^/processes/?" + - pattern: "^/notifications/" + - pattern: "^/statistics/?" + - pattern: "^/access-control/" + - pattern: "^/health$" + + # Whether to enable rendering of Search component on SSR. + # If set to true the component will be included in the HTML returned from the server side rendering. + # If set to false the component will not be included in the HTML returned from the server side rendering. + enableSearchComponent: false + # Whether to enable rendering of Browse component on SSR. + # If set to true the component will be included in the HTML returned from the server side rendering. + # If set to false the component will not be included in the HTML returned from the server side rendering. + enableBrowseComponent: false + # Enable state transfer from the server-side application to the client-side application. + # Defaults to true. + # Note: When using an external application cache layer, it's recommended not to transfer the state to avoid caching it. + # Disabling it ensures that dynamic state information is not inadvertently cached, which can improve security and + # ensure that users always use the most up-to-date state. + transferState: true + # When a different REST base URL is used for the server-side application, the generated state contains references to + # REST resources with the internal URL configured. By default, these internal URLs are replaced with public URLs. + # Disable this setting to avoid URL replacement during SSR. In this the state is not transferred to avoid security issues. + replaceRestUrl: true # The REST API server settings -# NOTE: these must be 'synced' with the 'dspace.server.url' setting in your backend's local.cfg. +# NOTE: these settings define which (publicly available) REST API to use. They are usually +# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg. rest: ssl: true - host: api7.dspace.org + host: demo.dspace.org port: 443 # NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript nameSpace: /server + # Provide a different REST url to be used during SSR execution. It must contain the whole url including protocol, server port and + # server namespace (uncomment to use it). + #ssrBaseUrl: http://localhost:8080/server # Caching settings cache: # NOTE: how long should objects be cached for by default msToLive: default: 900000 # 15 minutes - control: max-age=60 # revalidate browser + # Default 'Cache-Control' HTTP Header to set for all static content (including compiled *.js files) + # Defaults to max-age=604,800 seconds (one week). This lets a user's browser know that it can cache these + # files for one week, after which they will be "stale" and need to be redownloaded. + # NOTE: When updates are made to compiled *.js files, it will automatically bypass this browser cache, because + # all compiled *.js files include a unique hash in their name which updates when content is modified. + control: max-age=604800 # revalidate browser autoSync: defaultTime: 0 maxBufferSize: 100 timePerMethod: PATCH: 3 # time in seconds + # In-memory cache(s) of server-side rendered pages. These caches will store the most recently accessed public pages. + # Pages are automatically added/dropped from these caches based on how recently they have been used. + # Restarting the app clears all page caches. + # NOTE: To control the cache size, use the "max" setting. Keep in mind, individual cached pages are usually small (<100KB). + # Enabling *both* caches will mean that a page may be cached twice, once in each cache (but may expire at different times via timeToLive). + serverSide: + # Set to true to see all cache hits/misses/refreshes in your console logs. Useful for debugging SSR caching issues. + debug: false + # When enabled (i.e. max > 0), known bots will be sent pages from a server side cache specific for bots. + # (Keep in mind, bot detection cannot be guarranteed. It is possible some bots will bypass this cache.) + botCache: + # Maximum number of pages to cache for known bots. Set to zero (0) to disable server side caching for bots. + # Default is 1000, which means the 1000 most recently accessed public pages will be cached. + # As all pages are cached in server memory, increasing this value will increase memory needs. + # Individual cached pages are usually small (<100KB), so max=1000 should only require ~100MB of memory. + max: 1000 + # Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached + # copy is automatically refreshed on the next request. + # NOTE: For the bot cache, this setting may impact how quickly search engine bots will index new content on your site. + # For example, setting this to one week may mean that search engine bots may not find all new content for one week. + timeToLive: 86400000 # 1 day + # When set to true, after timeToLive expires, the next request will receive the *cached* page & then re-render the page + # behind the scenes to update the cache. This ensures users primarily interact with the cache, but may receive stale pages (older than timeToLive). + # When set to false, after timeToLive expires, the next request will wait on SSR to complete & receive a fresh page (which is then saved to cache). + # This ensures stale pages (older than timeToLive) are never returned from the cache, but some users will wait on SSR. + allowStale: true + # When enabled (i.e. max > 0), all anonymous users will be sent pages from a server side cache. + # This allows anonymous users to interact more quickly with the site, but also means they may see slightly + # outdated content (based on timeToLive) + anonymousCache: + # Maximum number of pages to cache. Default is zero (0) which means anonymous user cache is disabled. + # As all pages are cached in server memory, increasing this value will increase memory needs. + # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. + max: 0 + # Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached + # copy is automatically refreshed on the next request. + # NOTE: For the anonymous cache, it is recommended to keep this value low to avoid anonymous users seeing outdated content. + timeToLive: 10000 # 10 seconds + # When set to true, after timeToLive expires, the next request will receive the *cached* page & then re-render the page + # behind the scenes to update the cache. This ensures users primarily interact with the cache, but may receive stale pages (older than timeToLive). + # When set to false, after timeToLive expires, the next request will wait on SSR to complete & receive a fresh page (which is then saved to cache). + # This ensures stale pages (older than timeToLive) are never returned from the cache, but some users will wait on SSR. + allowStale: true # Authentication settings auth: @@ -51,6 +149,8 @@ auth: # Form settings form: + # Sets the spellcheck textarea attribute value + spellCheck: true # NOTE: Map server-side validators to comparative Angular form validators validatorMap: required: required @@ -115,42 +215,82 @@ languages: - code: en label: English active: true + - code: ar + label: العربية + active: true + - code: bn + label: বাংলা + active: true + - code: ca + label: Català + active: true - code: cs label: Čeština active: true - code: de label: Deutsch active: true + - code: el + label: Ελληνικά + active: true - code: es label: Español active: true + - code: fi + label: Suomi + active: true - code: fr label: Français active: true - code: gd label: Gàidhlig active: true - - code: lv - label: Latviešu + - code: hi + label: हिंदी active: true - code: hu label: Magyar active: true + - code: it + label: Italiano + active: true + - code: kk + label: Қазақ + active: true + - code: lv + label: Latviešu + active: true - code: nl label: Nederlands active: true + - code: pl + label: Polski + active: true - code: pt-PT label: Português active: true - code: pt-BR label: Português do Brasil active: true - - code: fi - label: Suomi + - code: sr-lat + label: Srpski (lat) active: true - - code: bn - label: বাংলা + - code: sr-cyr + label: Српски + active: true + - code: sv + label: Svenska + active: true + - code: tr + label: Türkçe active: true + - code: uk + label: Yкраї́нська + active: true + - code: vi + label: Tiếng Việt + active: true + # Browse-By Pages browseBy: @@ -160,11 +300,39 @@ browseBy: fiveYearLimit: 30 # The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items) defaultLowerLimit: 1900 + # If true, thumbnail images for items will be added to BOTH search and browse result lists. + showThumbnails: true + # The number of entries in a paginated browse results list. + # Rounded to the nearest size in the list of selectable sizes on the + # settings menu. + pageSize: 20 + +communityList: + # No. of communities to list per expansion (show more) + pageSize: 20 + +homePage: + recentSubmissions: + # The number of item showing in recent submission components + pageSize: 5 + # Sort record of recent submission + sortField: 'dc.date.accessioned' + topLevelCommunityList: + # No. of communities to list per page on the home page + # This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10 + pageSize: 5 -# Item Page Config +# Item Config item: edit: undoTimeout: 10000 # 10 seconds + # Show the item access status label in items lists + showAccessStatuses: false + bitstream: + # Number of entries in the bitstream list in the item view page. + # Rounded to the nearest size in the list of selectable sizes on the + # settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'. + pageSize: 5 # Collection Page Config collection: @@ -179,33 +347,33 @@ themes: # # # A theme with a handle property will match the community, collection or item with the given # # handle, and all collections and/or items within it - # - name: 'custom', - # handle: '10673/1233' + # - name: custom + # handle: 10673/1233 # # # A theme with a regex property will match the route using a regular expression. If it # # matches the route for a community or collection it will also apply to all collections # # and/or items within it - # - name: 'custom', - # regex: 'collections\/e8043bc2.*' + # - name: custom + # regex: collections\/e8043bc2.* # # # A theme with a uuid property will match the community, collection or item with the given # # ID, and all collections and/or items within it - # - name: 'custom', - # uuid: '0958c910-2037-42a9-81c7-dca80e3892b4' + # - name: custom + # uuid: 0958c910-2037-42a9-81c7-dca80e3892b4 # # # The extends property specifies an ancestor theme (by name). Whenever a themed component is not found # # in the current theme, its ancestor theme(s) will be checked recursively before falling back to default. - # - name: 'custom-A', - # extends: 'custom-B', + # - name: custom-A + # extends: custom-B # # Any of the matching properties above can be used - # handle: '10673/34' + # handle: 10673/34 # - # - name: 'custom-B', - # extends: 'custom', - # handle: '10673/12' + # - name: custom-B + # extends: custom + # handle: 10673/12 # # # A theme with only a name will match every route - # name: 'custom' + # name: custom # # # This theme will use the default bootstrap styling for DSpace components # - name: BASE_THEME_NAME @@ -231,9 +399,71 @@ themes: rel: manifest href: assets/dspace/images/favicons/manifest.webmanifest +# The default bundles that should always be displayed as suggestions when you upload a new bundle +bundle: + standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ] + # Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video'). # For images, this enables a gallery viewer where you can zoom or page through images. # For videos, this enables embedded video streaming mediaViewer: image: false video: false + +# Whether the end user agreement is required before users use the repository. +# If enabled, the user will be required to accept the agreement before they can use the repository. +# And whether the privacy statement should exist or not. +info: + enableEndUserAgreement: true + enablePrivacyStatement: true + +# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/) +# display in supported metadata fields. By default, only dc.description.abstract is supported. +markdown: + enabled: false + mathjax: false + +# Which vocabularies should be used for which search filters +# and whether to show the filter in the search sidebar +# Take a look at the filter-vocabulary-config.ts file for documentation on how the options are obtained +vocabularies: + - filter: 'subject' + vocabulary: 'srsc' + enabled: true + +# Default collection/community sorting order at Advanced search, Create/update community and collection when there are not a query. +comcolSelectionSort: + sortField: 'dc.title' + sortDirection: 'ASC' + +# Live Region configuration +# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms: +# Live regions are perceivable regions of a web page that are typically updated as a +# result of an external event when user focus may be elsewhere. +# +# The DSpace live region is a component present at the bottom of all pages that is invisible by default, but is useful +# for screen readers. Any message pushed to the live region will be announced by the screen reader. These messages +# usually contain information about changes on the page that might not be in focus. +liveRegion: + # The duration after which messages disappear from the live region in milliseconds + messageTimeOutDurationMs: 30000 + # The visibility of the live region. Setting this to true is only useful for debugging purposes. + isVisible: false + + +# Search settings +search: + # Number used to render n UI elements called loading skeletons that act as placeholders. + # These elements indicate that some content will be loaded in their stead. + # Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count. + # e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved. + defaultFiltersCount: 5 + +# Configuration for storing accessibility settings, used by the AccessibilitySettingsService +accessibility: + # The duration in days after which the accessibility settings cookie expires + cookieExpirationDuration: 7 + +matomo: + hostUrl: http://localhost:8135/ + siteId: 1 diff --git a/config/config.lindat.ui.tests.json b/config/config.lindat.ui.tests.json new file mode 100644 index 00000000000..c9bba0dc46c --- /dev/null +++ b/config/config.lindat.ui.tests.json @@ -0,0 +1,10 @@ +{ + "locators": { + "title": "LINDAT/CLARIAH-CZ Repository Home" + }, + "urls": { + "oaipmh_response": "https://dev-5.pc:8443/repository/server/oai/request?verb=ListRecords&metadataPrefix=oai_dc", + "redirect_to_login": "https://dev-5.pc:8443/repository/login?redirectUrl=http:%2F%2Fdev-5.pc:82%2Fhome", + "not_exposed_data": "https://dev-5.pc:8443/repository/server/oai/request?verb=GetRecord&metadataPrefix=cmdi&identifier=oai:dev-5.pc:20.500.12800/1-4740" + } +} diff --git a/config/config.yml b/config/config.yml index b5eecd112f0..9257bd2b09d 100644 --- a/config/config.yml +++ b/config/config.yml @@ -1,5 +1,173 @@ +debug: false + rest: - ssl: true - host: api7.dspace.org - port: 443 + ssl: false + host: localhost + port: 8080 nameSpace: /server + +universal: + # Whether to tell Angular to inline "critical" styles into the server-side rendered HTML. + # Determining which styles are critical is a relatively expensive operation; this option is + # disabled (false) by default to boost server performance at the expense of loading smoothness. + inlineCriticalCss: false + # Patterns to be run as regexes against the path of the page to check if SSR is allowed. + # If the path match any of the regexes it will be served directly in CSR. + # By default, excludes community and collection browse, global browse, global search, community list, statistics and various administrative tools. + excludePathPatterns: + - pattern: "^/communities/[a-f0-9-]{36}/browse(/.*)?$" + flag: "i" + - pattern: "^/collections/[a-f0-9-]{36}/browse(/.*)?$" + flag: "i" + - pattern: "^/browse/" + - pattern: "^/search$" + - pattern: "^/community-list$" + - pattern: "^/admin/" + - pattern: "^/processes/?" + - pattern: "^/notifications/" + - pattern: "^/statistics/?" + - pattern: "^/access-control/" + - pattern: "^/health$" + + # Whether to enable rendering of Search component on SSR. + # If set to true the component will be included in the HTML returned from the server side rendering. + # If set to false the component will not be included in the HTML returned from the server side rendering. + enableSearchComponent: false + # Whether to enable rendering of Browse component on SSR. + # If set to true the component will be included in the HTML returned from the server side rendering. + # If set to false the component will not be included in the HTML returned from the server side rendering. + enableBrowseComponent: false + +# Caching settings +cache: + # NOTE: how long should objects be cached for by default + msToLive: + default: 900000 # 15 minutes + # Default 'Cache-Control' HTTP Header to set for all static content (including compiled *.js files) + # Defaults to max-age=604,800 seconds (one week). This lets a user's browser know that it can cache these + # files for one week, after which they will be "stale" and need to be redownloaded. + # NOTE: When updates are made to compiled *.js files, it will automatically bypass this browser cache, because + # all compiled *.js files include a unique hash in their name which updates when content is modified. + control: max-age=604800 # revalidate browser + autoSync: + defaultTime: 0 + maxBufferSize: 100 + timePerMethod: + PATCH: 3 # time in seconds + # In-memory cache(s) of server-side rendered pages. These caches will store the most recently accessed public pages. + # Pages are automatically added/dropped from these caches based on how recently they have been used. + # Restarting the app clears all page caches. + # NOTE: To control the cache size, use the "max" setting. Keep in mind, individual cached pages are usually small (<100KB). + # Enabling *both* caches will mean that a page may be cached twice, once in each cache (but may expire at different times via timeToLive). + serverSide: + # Set to true to see all cache hits/misses/refreshes in your console logs. Useful for debugging SSR caching issues. + debug: false + # When enabled (i.e. max > 0), known bots will be sent pages from a server side cache specific for bots. + # (Keep in mind, bot detection cannot be guarranteed. It is possible some bots will bypass this cache.) + botCache: + # Maximum number of pages to cache for known bots. Set to zero (0) to disable server side caching for bots. + # Default is 1000, which means the 1000 most recently accessed public pages will be cached. + # As all pages are cached in server memory, increasing this value will increase memory needs. + # Individual cached pages are usually small (<100KB), so max=1000 should only require ~100MB of memory. + max: 2000 + # Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached + # copy is automatically refreshed on the next request. + # NOTE: For the bot cache, this setting may impact how quickly search engine bots will index new content on your site. + # For example, setting this to one week may mean that search engine bots may not find all new content for one week. + timeToLive: 86400000 # 1 day + # When set to true, after timeToLive expires, the next request will receive the *cached* page & then re-render the page + # behind the scenes to update the cache. This ensures users primarily interact with the cache, but may receive stale pages (older than timeToLive). + # When set to false, after timeToLive expires, the next request will wait on SSR to complete & receive a fresh page (which is then saved to cache). + # This ensures stale pages (older than timeToLive) are never returned from the cache, but some users will wait on SSR. + allowStale: true + # When enabled (i.e. max > 0), all anonymous users will be sent pages from a server side cache. + # This allows anonymous users to interact more quickly with the site, but also means they may see slightly + # outdated content (based on timeToLive) + anonymousCache: + # Maximum number of pages to cache. Default is zero (0) which means anonymous user cache is disabled. + # As all pages are cached in server memory, increasing this value will increase memory needs. + # Individual cached pages are usually small (<100KB), so a value of max=1000 would only require ~100MB of memory. + max: 200 + # Amount of time after which cached pages are considered stale (in ms). After becoming stale, the cached + # copy is automatically refreshed on the next request. + # NOTE: For the anonymous cache, it is recommended to keep this value low to avoid anonymous users seeing outdated content. + timeToLive: 10000 # 10 seconds + # When set to true, after timeToLive expires, the next request will receive the *cached* page & then re-render the page + # behind the scenes to update the cache. This ensures users primarily interact with the cache, but may receive stale pages (older than timeToLive). + # When set to false, after timeToLive expires, the next request will wait on SSR to complete & receive a fresh page (which is then saved to cache). + # This ensures stale pages (older than timeToLive) are never returned from the cache, but some users will wait on SSR. + allowStale: true + +#info: +# # Whether the end user agreement is required before users may use the repository. +# # If enabled, the user will be required to accept the agreement before they can use the repository. +# # If disabled, the page will not exist and no agreement is required to use the repository +# enableEndUserAgreement: false +# # Whether the privacy statement should exist or not. +# enablePrivacyStatement: false + +# Allow only EN and CS languages +languages: + - code: en + label: English + active: true + - code: ca + label: Català + active: false + - code: cs + label: Čeština + active: true + - code: de + label: Deutsch + active: false + - code: es + label: Español + active: false + - code: fr + label: Français + active: false + - code: gd + label: Gàidhlig + active: false + - code: lv + label: Latviešu + active: false + - code: hu + label: Magyar + active: false + - code: nl + label: Nederlands + active: false + - code: pl + label: Polski + active: false + - code: pt-PT + label: Português + active: false + - code: pt-BR + label: Português do Brasil + active: false + - code: fi + label: Suomi + active: false + - code: sv + label: Svenska + active: false + - code: tr + label: Türkçe + active: false + - code: kk + label: Қазақ + active: false + - code: bn + label: বাংলা + active: false + - code: hi + label: हिंदी + active: false + - code: el + label: Ελληνικά + active: false + - code: uk + label: Yкраї́нська + active: false diff --git a/cypress.config.ts b/cypress.config.ts new file mode 100644 index 00000000000..28463a44c61 --- /dev/null +++ b/cypress.config.ts @@ -0,0 +1,59 @@ +import { defineConfig } from 'cypress'; + +export default defineConfig({ + video: true, + videosFolder: 'cypress/videos', + screenshotsFolder: 'cypress/screenshots', + fixturesFolder: 'cypress/fixtures', + retries: { + runMode: 2, + openMode: 0, + }, + env: { + // Global DSpace environment variables used in all our Cypress e2e tests + // May be modified in this config, or overridden in a variety of ways. + // See Cypress environment variable docs: https://docs.cypress.io/guides/guides/environment-variables + // Default values listed here are all valid for the Demo Entities Data set available at + // https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data + // (This is the data set used in our CI environment) + + // Admin account used for administrative tests + DSPACE_TEST_ADMIN_USER: 'dspacedemo+admin@gmail.com', + DSPACE_TEST_ADMIN_USER_UUID: '335647b6-8a52-4ecb-a8c1-7ebabb199bda', + DSPACE_TEST_ADMIN_PASSWORD: 'dspace', + // Community/collection/publication used for view/edit tests + DSPACE_TEST_COMMUNITY: '0958c910-2037-42a9-81c7-dca80e3892b4', + DSPACE_TEST_COLLECTION: '282164f5-d325-4740-8dd1-fa4d6d3e7200', + DSPACE_TEST_ENTITY_PUBLICATION: '6160810f-1e53-40db-81ef-f6621a727398', + // Search term (should return results) used in search tests + DSPACE_TEST_SEARCH_TERM: 'test', + // Main Collection used for submission tests. Should be able to accept normal Item objects + DSPACE_TEST_SUBMIT_COLLECTION_NAME: 'Sample Collection', + DSPACE_TEST_SUBMIT_COLLECTION_UUID: '9d8334e9-25d3-4a67-9cea-3dffdef80144', + // Collection used for Person entity submission tests. MUST be configured with EntityType=Person. + DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME: 'People', + // Account used to test basic submission process + DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com', + DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace', + // Administrator users group + DSPACE_ADMINISTRATOR_GROUP: 'e59f5659-bff9-451e-b28f-439e7bd467e4', + CLARIN_TEST_WITHDRAWN_ITEM: '7282fc76-0941-4055-a5a3-1f582c638050', + CLARIN_TEST_WITHDRAWN_ITEM_WITH_REASON: '8ae76fcf-b26b-42f2-84d3-9a85e0517bca', + CLARIN_TEST_WITHDRAWN_ITEM_WITH_REASON_AND_AUTHORS: 'cd368b6a-0019-4813-bad9-5050e50ba36d', + CLARIN_TEST_WITHDRAWN_REPLACED_ITEM: '566b1b8b-840d-476c-9fb0-b92fb92d4aad', + CLARIN_TEST_WITHDRAWN_REPLACED_ITEM_WITH_AUTHORS: '600a9e09-dd31-428e-9328-2ed6631aa50a', + CLARIN_TEST_WITHDRAWN_REASON: 'reason', + CLARIN_TEST_WITHDRAWN_REPLACEMENT: 'new URL', + CLARIN_TEST_WITHDRAWN_AUTHORS: 'author1, author2' + }, + e2e: { + // Setup our plugins for e2e tests + setupNodeEvents(on, config) { + return require('./cypress/plugins/index.ts')(on, config); + }, + // This is the base URL that Cypress will run all tests against + // It can be overridden via the CYPRESS_BASE_URL environment variable + // (By default we set this to a value which should work in most development environments) + baseUrl: 'http://localhost:4000', + }, +}); diff --git a/cypress.json b/cypress.json deleted file mode 100644 index 80358eb6dde..00000000000 --- a/cypress.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "integrationFolder": "cypress/integration", - "supportFile": "cypress/support/index.ts", - "videosFolder": "cypress/videos", - "screenshotsFolder": "cypress/screenshots", - "pluginsFile": "cypress/plugins/index.ts", - "fixturesFolder": "cypress/fixtures", - "baseUrl": "http://localhost:4000", - "retries": { - "runMode": 2, - "openMode": 0 - }, - "env": { - "DSPACE_TEST_ADMIN_USER": "dspacedemo+admin@gmail.com", - "DSPACE_TEST_ADMIN_PASSWORD": "dspace", - "DSPACE_TEST_COMMUNITY": "0958c910-2037-42a9-81c7-dca80e3892b4", - "DSPACE_TEST_COLLECTION": "282164f5-d325-4740-8dd1-fa4d6d3e7200", - "DSPACE_TEST_ENTITY_PUBLICATION": "e98b0f27-5c19-49a0-960d-eb6ad5287067", - "DSPACE_TEST_SEARCH_TERM": "test", - "DSPACE_TEST_SUBMIT_COLLECTION_NAME": "Sample Collection", - "DSPACE_TEST_SUBMIT_COLLECTION_UUID": "9d8334e9-25d3-4a67-9cea-3dffdef80144", - "DSPACE_TEST_SUBMIT_USER": "dspacedemo+submit@gmail.com", - "DSPACE_TEST_SUBMIT_USER_PASSWORD": "dspace" - } -} \ No newline at end of file diff --git a/cypress/.gitignore b/cypress/.gitignore index 99bd2a6312f..645beff45f9 100644 --- a/cypress/.gitignore +++ b/cypress/.gitignore @@ -1,2 +1,3 @@ screenshots/ videos/ +downloads/ diff --git a/cypress/e2e/admin-add-new-modals.cy.ts b/cypress/e2e/admin-add-new-modals.cy.ts new file mode 100644 index 00000000000..332d44da138 --- /dev/null +++ b/cypress/e2e/admin-add-new-modals.cy.ts @@ -0,0 +1,54 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Admin Add New Modals', () => { + beforeEach(() => { + // Must login as an Admin for sidebar to appear + cy.visit('/login'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('Add new Community modal should pass accessibility tests', () => { + // Pin the sidebar open + cy.get('#sidebar-collapse-toggle').trigger('mouseover'); + cy.get('#sidebar-collapse-toggle').click(); + + // Click on entry of menu + cy.get('#admin-menu-section-new-title').should('be.visible'); + cy.get('#admin-menu-section-new-title').click(); + + cy.get('a[data-test="menu.section.new_community"]').click(); + + // Analyze for accessibility + testA11y('ds-create-community-parent-selector'); + }); + + it('Add new Collection modal should pass accessibility tests', () => { + // Pin the sidebar open + cy.get('#sidebar-collapse-toggle').trigger('mouseover'); + cy.get('#sidebar-collapse-toggle').click(); + + // Click on entry of menu + cy.get('#admin-menu-section-new-title').should('be.visible'); + cy.get('#admin-menu-section-new-title').click(); + + cy.get('a[data-test="menu.section.new_collection"]').click(); + + // Analyze for accessibility + testA11y('ds-create-collection-parent-selector'); + }); + + it('Add new Item modal should pass accessibility tests', () => { + // Pin the sidebar open + cy.get('#sidebar-collapse-toggle').trigger('mouseover'); + cy.get('#sidebar-collapse-toggle').click(); + + // Click on entry of menu + cy.get('#admin-menu-section-new-title').should('be.visible'); + cy.get('#admin-menu-section-new-title').click(); + + cy.get('a[data-test="menu.section.new_item"]').click(); + + // Analyze for accessibility + testA11y('ds-create-item-parent-selector'); + }); +}); diff --git a/cypress/e2e/admin-curation-tasks.cy.ts b/cypress/e2e/admin-curation-tasks.cy.ts new file mode 100644 index 00000000000..e66f0ccaad8 --- /dev/null +++ b/cypress/e2e/admin-curation-tasks.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Admin Curation Tasks', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/curation-tasks'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Page must first be visible + cy.get('ds-admin-curation-task').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-admin-curation-task'); + }); +}); diff --git a/cypress/e2e/admin-edit-modals.cy.ts b/cypress/e2e/admin-edit-modals.cy.ts new file mode 100644 index 00000000000..8ba524d5be1 --- /dev/null +++ b/cypress/e2e/admin-edit-modals.cy.ts @@ -0,0 +1,54 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Admin Edit Modals', () => { + beforeEach(() => { + // Must login as an Admin for sidebar to appear + cy.visit('/login'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('Edit Community modal should pass accessibility tests', () => { + // Pin the sidebar open + cy.get('#sidebar-collapse-toggle').trigger('mouseover'); + cy.get('#sidebar-collapse-toggle').click(); + + // Click on entry of menu + cy.get('#admin-menu-section-edit-title').should('be.visible'); + cy.get('#admin-menu-section-edit-title').click(); + + cy.get('a[data-test="menu.section.edit_community"]').click(); + + // Analyze for accessibility + testA11y('ds-edit-community-selector'); + }); + + it('Edit Collection modal should pass accessibility tests', () => { + // Pin the sidebar open + cy.get('#sidebar-collapse-toggle').trigger('mouseover'); + cy.get('#sidebar-collapse-toggle').click(); + + // Click on entry of menu + cy.get('#admin-menu-section-edit-title').should('be.visible'); + cy.get('#admin-menu-section-edit-title').click(); + + cy.get('a[data-test="menu.section.edit_collection"]').click(); + + // Analyze for accessibility + testA11y('ds-edit-collection-selector'); + }); + + it('Edit Item modal should pass accessibility tests', () => { + // Pin the sidebar open + cy.get('#sidebar-collapse-toggle').trigger('mouseover'); + cy.get('#sidebar-collapse-toggle').click(); + + // Click on entry of menu + cy.get('#admin-menu-section-edit-title').should('be.visible'); + cy.get('#admin-menu-section-edit-title').click(); + + cy.get('a[data-test="menu.section.edit_item"]').click(); + + // Analyze for accessibility + testA11y('ds-edit-item-selector'); + }); +}); diff --git a/cypress/e2e/admin-export-modals.cy.ts b/cypress/e2e/admin-export-modals.cy.ts new file mode 100644 index 00000000000..24a184fd35e --- /dev/null +++ b/cypress/e2e/admin-export-modals.cy.ts @@ -0,0 +1,39 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Admin Export Modals', () => { + beforeEach(() => { + // Must login as an Admin for sidebar to appear + cy.visit('/login'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('Export metadata modal should pass accessibility tests', () => { + // Pin the sidebar open + cy.get('#sidebar-collapse-toggle').trigger('mouseover'); + cy.get('#sidebar-collapse-toggle').click(); + + // Click on entry of menu + cy.get('#admin-menu-section-export-title').should('be.visible'); + cy.get('#admin-menu-section-export-title').click(); + + cy.get('a[data-test="menu.section.export_metadata"]').click(); + + // Analyze for accessibility + testA11y('ds-export-metadata-selector'); + }); + + it('Export batch modal should pass accessibility tests', () => { + // Pin the sidebar open + cy.get('#sidebar-collapse-toggle').trigger('mouseover'); + cy.get('#sidebar-collapse-toggle').click(); + + // Click on entry of menu + cy.get('#admin-menu-section-export-title').should('be.visible'); + cy.get('#admin-menu-section-export-title').click(); + + cy.get('a[data-test="menu.section.export_batch"]').click(); + + // Analyze for accessibility + testA11y('ds-export-metadata-selector'); + }); +}); diff --git a/cypress/e2e/admin-menu.cy.ts b/cypress/e2e/admin-menu.cy.ts new file mode 100644 index 00000000000..f400a2adc27 --- /dev/null +++ b/cypress/e2e/admin-menu.cy.ts @@ -0,0 +1,26 @@ +import { + TEST_ADMIN_PASSWORD, + TEST_ADMIN_USER, + TEST_SUBMIT_COLLECTION_UUID, +} from '../support/e2e'; + +/** + * Test menu options for admin + */ +describe('Admin Menu Page', () => { + beforeEach(() => { + // Create a new submission + cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + }); + + it('should pass accessibility tests', () => { + // Check handles redirect url in the tag + cy.get('ds-admin-sidebar-section a[href = "/handle-table"]').scrollIntoView().should('be.visible'); + + // Check licenses redirect url in the tag + cy.get('ds-admin-sidebar-section a[href = "/licenses/manage-table"]').scrollIntoView().should('be.visible'); + }); +}); diff --git a/cypress/e2e/admin-search-page.cy.ts b/cypress/e2e/admin-search-page.cy.ts new file mode 100644 index 00000000000..38b02b92f0c --- /dev/null +++ b/cypress/e2e/admin-search-page.cy.ts @@ -0,0 +1,20 @@ +describe('Admin Search Page', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/search'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + //Page must first be visible + cy.get('ds-admin-search-page').should('be.visible'); + // At least one search result should be displayed + cy.get('[data-test="list-object"]').should('be.visible'); + // Click each filter toggle to open *every* filter + // (As we want to scan filter section for accessibility issues as well) + cy.get('[data-test="filter-toggle"]').click({ multiple: true }); + // Analyze for accessibility issues + // CLARIN-DSpace still has some accessibility issues, so we will not fail the test + // testA11y('ds-admin-search-page'); + }); +}); diff --git a/cypress/e2e/admin-sidebar.cy.ts b/cypress/e2e/admin-sidebar.cy.ts new file mode 100644 index 00000000000..7612eb53132 --- /dev/null +++ b/cypress/e2e/admin-sidebar.cy.ts @@ -0,0 +1,28 @@ +import { Options } from 'cypress-axe'; +import { testA11y } from 'cypress/support/utils'; + +describe('Admin Sidebar', () => { + beforeEach(() => { + // Must login as an Admin for sidebar to appear + cy.visit('/login'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should be pinnable and pass accessibility tests', () => { + // Pin the sidebar open + cy.get('#sidebar-collapse-toggle').click(); + + // Click on every expandable section to open all menus + cy.get('ds-expandable-admin-sidebar-section').click({multiple: true}); + + // Analyze for accessibility + testA11y('ds-admin-sidebar', + { + rules: { + // Currently all expandable sections have nested interactive elements + // See https://github.com/DSpace/dspace-angular/issues/2178 + 'nested-interactive': { enabled: false }, + } + } as Options); + }); +}); diff --git a/cypress/e2e/admin-workflow-page.cy.ts b/cypress/e2e/admin-workflow-page.cy.ts new file mode 100644 index 00000000000..4feff0bee0f --- /dev/null +++ b/cypress/e2e/admin-workflow-page.cy.ts @@ -0,0 +1,20 @@ +describe('Admin Workflow Page', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/workflow'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Page must first be visible + cy.get('ds-admin-workflow-page').should('be.visible'); + // At least one search result should be displayed + cy.get('[data-test="list-object"]').should('be.visible'); + // Click each filter toggle to open *every* filter + // (As we want to scan filter section for accessibility issues as well) + cy.get('[data-test="filter-toggle"]').click({ multiple: true }); + // Analyze for accessibility issues + // CLARIN-DSpace still has some accessibility issues, so we will not fail the test + // testA11y('ds-admin-workflow-page'); + }); +}); diff --git a/cypress/e2e/batch-import-page.cy.ts b/cypress/e2e/batch-import-page.cy.ts new file mode 100644 index 00000000000..871b8644ce1 --- /dev/null +++ b/cypress/e2e/batch-import-page.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Batch Import Page', () => { + beforeEach(() => { + // Must login as an Admin to see processes + cy.visit('/admin/batch-import'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Batch import form must first be visible + cy.get('ds-batch-import-page').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-batch-import-page'); + }); +}); diff --git a/cypress/e2e/bitstreams-format.cy.ts b/cypress/e2e/bitstreams-format.cy.ts new file mode 100644 index 00000000000..f113d45ebce --- /dev/null +++ b/cypress/e2e/bitstreams-format.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Bitstreams Formats', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/registries/bitstream-formats'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Page must first be visible + cy.get('ds-bitstream-formats').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-bitstream-formats'); + }); +}); diff --git a/cypress/e2e/breadcrumbs.cy.ts b/cypress/e2e/breadcrumbs.cy.ts new file mode 100644 index 00000000000..0cddbc723c6 --- /dev/null +++ b/cypress/e2e/breadcrumbs.cy.ts @@ -0,0 +1,14 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Breadcrumbs', () => { + it('should pass accessibility tests', () => { + // Visit an Item, as those have more breadcrumbs + cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'))); + + // Wait for breadcrumbs to be visible + cy.get('ds-breadcrumbs').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-breadcrumbs'); + }); +}); diff --git a/cypress/e2e/browse-by-author.cy.ts b/cypress/e2e/browse-by-author.cy.ts new file mode 100644 index 00000000000..77af31fb2f2 --- /dev/null +++ b/cypress/e2e/browse-by-author.cy.ts @@ -0,0 +1,14 @@ + +describe('Browse By Author', () => { + it('should pass accessibility tests', () => { + cy.visit('/browse/author'); + + // Wait for to be visible + cy.get('ds-browse-by-metadata-page').should('be.visible'); + + // Analyze for accessibility + // CLARIN + // testA11y('ds-browse-by-metadata-page'); + // CLARIN + }); +}); diff --git a/cypress/e2e/browse-by-dateissued.cy.ts b/cypress/e2e/browse-by-dateissued.cy.ts new file mode 100644 index 00000000000..e5211210f31 --- /dev/null +++ b/cypress/e2e/browse-by-dateissued.cy.ts @@ -0,0 +1,12 @@ +describe('Browse By Date Issued', () => { + it('should pass accessibility tests', () => { + cy.visit('/browse/dateissued'); + + // Wait for to be visible + cy.get('ds-browse-by-date-page').should('be.visible'); + + // Removed the accessibility tests because the whole UI is customized + // Analyze for accessibility + // testA11y('ds-browse-by-date-page'); + }); +}); diff --git a/cypress/e2e/browse-by-subject.cy.ts b/cypress/e2e/browse-by-subject.cy.ts new file mode 100644 index 00000000000..7463e3fe170 --- /dev/null +++ b/cypress/e2e/browse-by-subject.cy.ts @@ -0,0 +1,15 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Browse By Subject', () => { + it('should pass accessibility tests', () => { + cy.visit('/browse/subject'); + + // Wait for to be visible + cy.get('ds-browse-by-metadata-page').should('be.visible'); + + // Analyze for accessibility + // CLARIN + // testA11y('ds-browse-by-metadata-page'); + // CLARIN + }); +}); diff --git a/cypress/e2e/browse-by-title.cy.ts b/cypress/e2e/browse-by-title.cy.ts new file mode 100644 index 00000000000..3cde7d4c7cf --- /dev/null +++ b/cypress/e2e/browse-by-title.cy.ts @@ -0,0 +1,12 @@ +describe('Browse By Title', () => { + it('should pass accessibility tests', () => { + cy.visit('/browse/title'); + + // Wait for to be visible + cy.get('ds-browse-by-title-page').should('be.visible'); + + // Removed the accessibility tests because the whole UI is customized + // Analyze for accessibility + // testA11y('ds-browse-by-title-page'); + }); +}); diff --git a/cypress/e2e/bulk-access.cy.ts b/cypress/e2e/bulk-access.cy.ts new file mode 100644 index 00000000000..c8921f5651c --- /dev/null +++ b/cypress/e2e/bulk-access.cy.ts @@ -0,0 +1,29 @@ +describe('Bulk Access', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/bulk-access'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Page must first be visible + cy.get('ds-bulk-access').should('be.visible'); + // At least one search result should be displayed + cy.get('[data-test="list-object"]').should('be.visible'); + // Click each filter toggle to open *every* filter + // (As we want to scan filter section for accessibility issues as well) + cy.get('[data-test="filter-toggle"]').click({ multiple: true }); + // Analyze for accessibility issues + // CLARIN-DSpace still has some accessibility issues, so we will not fail the test + // testA11y('ds-bulk-access', { + // rules: { + // // All panels are accordians & fail "aria-required-children" and "nested-interactive". + // // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 + // 'aria-required-children': { enabled: false }, + // 'nested-interactive': { enabled: false }, + // // Card titles fail this test currently + // 'heading-order': { enabled: false }, + // }, + // } as Options); + }); +}); diff --git a/cypress/e2e/collection-edit.cy.ts b/cypress/e2e/collection-edit.cy.ts new file mode 100644 index 00000000000..63d873db3ec --- /dev/null +++ b/cypress/e2e/collection-edit.cy.ts @@ -0,0 +1,128 @@ +import { testA11y } from 'cypress/support/utils'; + +const COLLECTION_EDIT_PAGE = '/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('/edit'); + +beforeEach(() => { + // All tests start with visiting the Edit Collection Page + cy.visit(COLLECTION_EDIT_PAGE); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); +}); + +describe('Edit Collection > Edit Metadata tab', () => { + it('should pass accessibility tests', () => { + // tag must be loaded + cy.get('ds-edit-collection').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-edit-collection'); + }); +}); + +describe('Edit Collection > Assign Roles tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="roles"]').click(); + + // tag must be loaded + cy.get('ds-collection-roles').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-collection-roles'); + }); +}); + +describe('Edit Collection > Content Source tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="source"]').click(); + + // tag must be loaded + cy.get('ds-collection-source').should('be.visible'); + + // Check the external source checkbox (to display all fields on the page) + cy.get('#externalSourceCheck').check(); + + // Wait for the source controls to appear + // cy.get('ds-collection-source-controls').should('be.visible'); + + // Analyze entire page for accessibility issues + testA11y('ds-collection-source'); + }); +}); + +describe('Edit Collection > Curate tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="curate"]').click(); + + // tag must be loaded + cy.get('ds-collection-curate').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-collection-curate'); + }); +}); + +describe('Edit Collection > Access Control tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="access-control"]').click(); + + // tag must be loaded + cy.get('ds-collection-access-control').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-collection-access-control'); + }); +}); + +describe('Edit Collection > Authorizations tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="authorizations"]').click(); + + // tag must be loaded + cy.get('ds-collection-authorizations').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-collection-authorizations'); + }); +}); + +describe('Edit Collection > Item Mapper tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="mapper"]').click(); + + // tag must be loaded + cy.get('ds-collection-item-mapper').should('be.visible'); + + // Analyze entire page for accessibility issues + testA11y('ds-collection-item-mapper'); + + // Click on the "Map new Items" tab + cy.get('li[data-test="mapTab"] a').click(); + + // Make sure search form is now visible + cy.get('ds-search-form').should('be.visible'); + + // Analyze entire page (again) for accessibility issues + testA11y('ds-collection-item-mapper'); + }); +}); + + +describe('Edit Collection > Delete page', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="delete-button"]').click(); + + // tag must be loaded + cy.get('ds-delete-collection').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-delete-collection'); + }); +}); diff --git a/cypress/e2e/collection-page.cy.ts b/cypress/e2e/collection-page.cy.ts new file mode 100644 index 00000000000..1e308be4767 --- /dev/null +++ b/cypress/e2e/collection-page.cy.ts @@ -0,0 +1,22 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Collection Page', () => { + + it('should pass accessibility tests', () => { + cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent'); + + // Visit Collections page + cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); + + // Wait for the "viewevent" to trigger on the Collection page. + // This ensures our tag is fully loaded, as the tag is contained within it. + cy.wait('@viewevent'); + + // tag must be loaded + cy.get('ds-collection-page').should('be.visible'); + + // TODO accessibility tests are failing because the UI has been changed + // Analyze for accessibility issues + // testA11y('ds-collection-page'); + }); +}); diff --git a/cypress/e2e/collection-statistics.cy.ts b/cypress/e2e/collection-statistics.cy.ts new file mode 100644 index 00000000000..99b6f8218b0 --- /dev/null +++ b/cypress/e2e/collection-statistics.cy.ts @@ -0,0 +1,38 @@ +import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; + +describe('Collection Statistics Page', () => { + const COLLECTIONSTATISTICSPAGE = '/statistics/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')); + + // NOTE: the statistics option was removed from the navbar - add it there in the future and uncomment this test + // it('should load if you click on "Statistics" from a Collection page', () => { + // cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'))); + // cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click(); + // cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE); + // }); + + it('should contain a "Total visits" section', () => { + cy.visit(COLLECTIONSTATISTICSPAGE); + cy.get('table[data-test="TotalVisits"]').should('be.visible'); + }); + + it('should contain a "Total visits per month" section', () => { + cy.visit(COLLECTIONSTATISTICSPAGE); + // Check just for existence because this table is empty in CI environment as it's historical data + cy.get('.'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('_TotalVisitsPerMonth')).should('exist'); + }); + + it('should pass accessibility tests', () => { + cy.visit(COLLECTIONSTATISTICSPAGE); + + // tag must be loaded + cy.get('ds-collection-statistics-page').should('be.visible'); + + // Verify / wait until "Total Visits" table's label is non-empty + // (This table loads these labels asynchronously, so we want to wait for them before analyzing page) + cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT); + + // Analyze for accessibility issues + testA11y('ds-collection-statistics-page'); + }); +}); diff --git a/cypress/e2e/community-edit.cy.ts b/cypress/e2e/community-edit.cy.ts new file mode 100644 index 00000000000..8fc1a7733e7 --- /dev/null +++ b/cypress/e2e/community-edit.cy.ts @@ -0,0 +1,86 @@ +import { testA11y } from 'cypress/support/utils'; + +const COMMUNITY_EDIT_PAGE = '/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')).concat('/edit'); + +beforeEach(() => { + // All tests start with visiting the Edit Community Page + cy.visit(COMMUNITY_EDIT_PAGE); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); +}); + +describe('Edit Community > Edit Metadata tab', () => { + it('should pass accessibility tests', () => { + // tag must be loaded + cy.get('ds-edit-community').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-edit-community'); + }); +}); + +describe('Edit Community > Assign Roles tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="roles"]').click(); + + // tag must be loaded + cy.get('ds-community-roles').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-community-roles'); + }); +}); + +describe('Edit Community > Curate tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="curate"]').click(); + + // tag must be loaded + cy.get('ds-community-curate').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-community-curate'); + }); +}); + +describe('Edit Community > Access Control tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="access-control"]').click(); + + // tag must be loaded + cy.get('ds-community-access-control').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-community-access-control'); + }); +}); + +describe('Edit Community > Authorizations tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="authorizations"]').click(); + + // tag must be loaded + cy.get('ds-community-authorizations').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-community-authorizations'); + }); +}); + +describe('Edit Community > Delete page', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="delete-button"]').click(); + + // tag must be loaded + cy.get('ds-delete-community').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-delete-community'); + }); +}); diff --git a/cypress/e2e/community-list.cy.ts b/cypress/e2e/community-list.cy.ts new file mode 100644 index 00000000000..c371f6ceae7 --- /dev/null +++ b/cypress/e2e/community-list.cy.ts @@ -0,0 +1,17 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Community List Page', () => { + + it('should pass accessibility tests', () => { + cy.visit('/community-list'); + + // tag must be loaded + cy.get('ds-community-list-page').should('be.visible'); + + // Open every expand button on page, so that we can scan sub-elements as well + cy.get('[data-test="expand-button"]').click({ multiple: true }); + + // Analyze for accessibility issues + testA11y('ds-community-list-page'); + }); +}); diff --git a/cypress/e2e/community-page.cy.ts b/cypress/e2e/community-page.cy.ts new file mode 100644 index 00000000000..8969e647058 --- /dev/null +++ b/cypress/e2e/community-page.cy.ts @@ -0,0 +1,15 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Community Page', () => { + + it('should pass accessibility tests', () => { + cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'))); + + // tag must be loaded + cy.get('ds-community-page').should('be.visible'); + + // TODO accessibility tests are failing because the UI has been changed + // Analyze for accessibility issues + // testA11y('ds-community-page'); + }); +}); diff --git a/cypress/e2e/community-statistics.cy.ts b/cypress/e2e/community-statistics.cy.ts new file mode 100644 index 00000000000..7cd7c6fc3c3 --- /dev/null +++ b/cypress/e2e/community-statistics.cy.ts @@ -0,0 +1,38 @@ +import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; + +describe('Community Statistics Page', () => { + const COMMUNITYSTATISTICSPAGE = '/statistics/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')); + + // NOTE: Statistics option was removed from the navbar + // it('should load if you click on "Statistics" from a Community page', () => { + // cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'))); + // cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click(); + // cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE); + // }); + + it('should contain a "Total visits" section', () => { + cy.visit(COMMUNITYSTATISTICSPAGE); + cy.get('table[data-test="TotalVisits"]').should('be.visible'); + }); + + it('should contain a "Total visits per month" section', () => { + cy.visit(COMMUNITYSTATISTICSPAGE); + // Check just for existence because this table is empty in CI environment as it's historical data + cy.get('.'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')).concat('_TotalVisitsPerMonth')).should('exist'); + }); + + it('should pass accessibility tests', () => { + cy.visit(COMMUNITYSTATISTICSPAGE); + + // tag must be loaded + cy.get('ds-community-statistics-page').should('be.visible'); + + // Verify / wait until "Total Visits" table's label is non-empty + // (This table loads these labels asynchronously, so we want to wait for them before analyzing page) + cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT); + + // Analyze for accessibility issues + testA11y('ds-community-statistics-page'); + }); +}); diff --git a/cypress/e2e/create-eperson.cy.ts b/cypress/e2e/create-eperson.cy.ts new file mode 100644 index 00000000000..d23986ba29d --- /dev/null +++ b/cypress/e2e/create-eperson.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Create Eperson', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/epeople/create'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Form must first be visible + cy.get('ds-eperson-form').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-eperson-form'); + }); +}); diff --git a/cypress/e2e/create-group.cy.ts b/cypress/e2e/create-group.cy.ts new file mode 100644 index 00000000000..135c041a8d5 --- /dev/null +++ b/cypress/e2e/create-group.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Create Group', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/groups/create'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Form must first be visible + cy.get('ds-group-form').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-group-form'); + }); +}); diff --git a/cypress/e2e/edit-eperson.cy.ts b/cypress/e2e/edit-eperson.cy.ts new file mode 100644 index 00000000000..166c913b8c8 --- /dev/null +++ b/cypress/e2e/edit-eperson.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Edit Eperson', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/epeople/'.concat(Cypress.env('DSPACE_TEST_ADMIN_USER_UUID')).concat('/edit')); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Form must first be visible + cy.get('ds-eperson-form').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-eperson-form'); + }); +}); diff --git a/cypress/e2e/edit-group.cy.ts b/cypress/e2e/edit-group.cy.ts new file mode 100644 index 00000000000..e43ede978ad --- /dev/null +++ b/cypress/e2e/edit-group.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Edit Group', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/groups/'.concat(Cypress.env('DSPACE_ADMINISTRATOR_GROUP')).concat('/edit')); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Form must first be visible + cy.get('ds-group-form').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-group-form'); + }); +}); diff --git a/cypress/e2e/end-user-agreement.cy.ts b/cypress/e2e/end-user-agreement.cy.ts new file mode 100644 index 00000000000..989d21ce60f --- /dev/null +++ b/cypress/e2e/end-user-agreement.cy.ts @@ -0,0 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('End User Agreement', () => { + it('should pass accessibility tests', () => { + cy.visit('/info/end-user-agreement'); + + // Page must first be visible + cy.get('ds-end-user-agreement').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-end-user-agreement'); + }); +}); diff --git a/cypress/e2e/epeople-registry.cy.ts b/cypress/e2e/epeople-registry.cy.ts new file mode 100644 index 00000000000..a6192f13d95 --- /dev/null +++ b/cypress/e2e/epeople-registry.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Epeople registry', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/epeople'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Epeople registry page must first be visible + cy.get('ds-epeople-registry').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-epeople-registry'); + }); +}); diff --git a/cypress/e2e/feedback.cy.ts b/cypress/e2e/feedback.cy.ts new file mode 100644 index 00000000000..75fe1097c63 --- /dev/null +++ b/cypress/e2e/feedback.cy.ts @@ -0,0 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Feedback', () => { + it('should pass accessibility tests', () => { + cy.visit('/info/feedback'); + + // Page must first be visible + cy.get('ds-feedback').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-feedback'); + }); +}); diff --git a/cypress/e2e/footer.cy.ts b/cypress/e2e/footer.cy.ts new file mode 100644 index 00000000000..156849519cd --- /dev/null +++ b/cypress/e2e/footer.cy.ts @@ -0,0 +1,14 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Footer', () => { + it('should pass accessibility tests', () => { + cy.visit('/'); + + // Footer must first be visible + cy.get('ds-footer').should('be.visible'); + + // TODO accessibility tests are failing because the UI has been changed + // Analyze for accessibility + // testA11y('ds-footer'); + }); +}); diff --git a/cypress/e2e/groups-registry.cy.ts b/cypress/e2e/groups-registry.cy.ts new file mode 100644 index 00000000000..5c0099c2f1f --- /dev/null +++ b/cypress/e2e/groups-registry.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Groups registry', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/access-control/groups'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Epeople registry page must first be visible + cy.get('ds-groups-registry').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-groups-registry'); + }); +}); diff --git a/cypress/e2e/handle-page.cy.ts b/cypress/e2e/handle-page.cy.ts new file mode 100644 index 00000000000..6c900e595d8 --- /dev/null +++ b/cypress/e2e/handle-page.cy.ts @@ -0,0 +1,26 @@ +import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER } from '../support/e2e'; + +/** + * Test for checking if the handle page is loaded after redirecting. + */ +describe('Handle Page', () => { + + it('should pass accessibility tests', { + retries: { + runMode: 8, + openMode: 8, + }, + defaultCommandTimeout: 10000 + }, () => { + cy.visit('/handle-table'); + cy.loginViaForm(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + // tag must be loaded + cy.get('ds-handle-page').should('exist'); + + // tag must be loaded + cy.get('ds-handle-table').should('exist'); + + // tag must be loaded + cy.get('ds-handle-global-actions').should('exist'); + }); +}); diff --git a/cypress/e2e/header.cy.ts b/cypress/e2e/header.cy.ts new file mode 100644 index 00000000000..f8440b274be --- /dev/null +++ b/cypress/e2e/header.cy.ts @@ -0,0 +1,15 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Header', () => { + it('should pass accessibility tests', () => { + cy.visit('/'); + + // Header must first be visible + cy.get('ds-header').should('be.visible'); + + // TODO accessibility tests are failing because the UI has been changed + // Analyze for accessibility + // testA11y('ds-header'); + }); + +}); diff --git a/cypress/e2e/health-page.cy.ts b/cypress/e2e/health-page.cy.ts new file mode 100644 index 00000000000..c702fa72d79 --- /dev/null +++ b/cypress/e2e/health-page.cy.ts @@ -0,0 +1,62 @@ +import { testA11y } from 'cypress/support/utils'; +import { Options } from 'cypress-axe'; + + +beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/health'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); +}); + +describe('Health Page > Status Tab', () => { + it('should pass accessibility tests', () => { + cy.intercept('GET', '/server/actuator/health').as('status'); + cy.wait('@status'); + + cy.get('a[data-test="health-page.status-tab"]').click(); + // Page must first be visible + cy.get('ds-health-page').should('be.visible'); + cy.get('ds-health-panel').should('be.visible'); + + // wait for all the ds-health-info-component components to be rendered + cy.get('div[role="tabpanel"]').each(($panel: HTMLDivElement) => { + cy.wrap($panel).find('ds-health-component').should('be.visible'); + }); + // Analyze for accessibility issues + testA11y('ds-health-page', { + rules: { + // All panels are accordians & fail "aria-required-children" and "nested-interactive". + // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 + 'aria-required-children': { enabled: false }, + 'nested-interactive': { enabled: false }, + }, + } as Options); + }); +}); + +describe('Health Page > Info Tab', () => { + it('should pass accessibility tests', () => { + cy.intercept('GET', '/server/actuator/info').as('info'); + cy.wait('@info'); + + cy.get('a[data-test="health-page.info-tab"]').click(); + // Page must first be visible + cy.get('ds-health-page').should('be.visible'); + cy.get('ds-health-info').should('be.visible'); + + // wait for all the ds-health-info-component components to be rendered + cy.get('div[role="tabpanel"]').each(($panel: HTMLDivElement) => { + cy.wrap($panel).find('ds-health-info-component').should('be.visible'); + }); + + // Analyze for accessibility issues + testA11y('ds-health-info', { + rules: { + // All panels are accordions & fail "aria-required-children" and "nested-interactive". + // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 + 'aria-required-children': { enabled: false }, + 'nested-interactive': { enabled: false }, + }, + } as Options); + }); +}); diff --git a/cypress/e2e/homepage-statistics.cy.ts b/cypress/e2e/homepage-statistics.cy.ts new file mode 100644 index 00000000000..e3f0c0cb41b --- /dev/null +++ b/cypress/e2e/homepage-statistics.cy.ts @@ -0,0 +1,34 @@ +// Commented out because exposing of the config.json was changed and the `generateViewEvent` cannot be used. + +// describe('Site Statistics Page', () => { +// // CLARIN +// // NOTE: statistics were removed from the navbar +// // it('should load if you click on "Statistics" from homepage', () => { +// // cy.visit('/'); +// // cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click(); +// // cy.location('pathname').should('eq', '/statistics'); +// // }); +// +// it('should pass accessibility tests', () => { +// // generate 2 view events on an Item's page +// cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item'); +// cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item'); +// +// cy.visit('/statistics'); +// +// // tag must be visable +// cy.get('ds-site-statistics-page').should('be.visible'); +// +// // Verify / wait until "Total Visits" table's *last* label is non-empty +// // (This table loads these labels asynchronously, so we want to wait for them before analyzing page) +// cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').last().contains(REGEX_MATCH_NON_EMPTY_TEXT); +// // Wait an extra 500ms, just so all entries in Total Visits have loaded. +// cy.wait(500); +// +// // Analyze for accessibility issues +// // CLARIN +// // NOTE: accessibility tests are failing because the UI has been changed +// // testA11y('ds-site-statistics-page'); +// // CLARIN +// }); +// }); diff --git a/cypress/e2e/homepage.cy.ts b/cypress/e2e/homepage.cy.ts new file mode 100644 index 00000000000..59582adb7bc --- /dev/null +++ b/cypress/e2e/homepage.cy.ts @@ -0,0 +1,33 @@ +import { testA11y } from 'cypress/support/utils'; + +// NOTE: We changed homepage and these tests are failing +// describe('Homepage', () => { +// beforeEach(() => { +// // All tests start with visiting homepage +// cy.visit('/'); +// }); +// +// it('should display translated title "DSpace Angular :: Home"', () => { +// cy.title().should('eq', 'DSpace Angular :: Home'); +// }); +// +// it('should contain a news section', () => { +// cy.get('ds-home-news').should('be.visible'); +// }); +// +// it('should have a working search box', () => { +// const queryString = 'test'; +// cy.get('[data-test="search-box"]').type(queryString); +// cy.get('[data-test="search-button"]').click(); +// cy.url().should('include', '/search'); +// cy.url().should('include', 'query=' + encodeURI(queryString)); +// }); +// +// it('should pass accessibility tests', () => { +// // Wait for homepage tag to appear +// cy.get('ds-home-page').should('be.visible'); +// +// // Analyze for accessibility issues +// testA11y('ds-home-page'); +// }); +// }); diff --git a/cypress/e2e/item-edit.cy.ts b/cypress/e2e/item-edit.cy.ts new file mode 100644 index 00000000000..ad5d8ea0930 --- /dev/null +++ b/cypress/e2e/item-edit.cy.ts @@ -0,0 +1,180 @@ +import { testA11y } from 'cypress/support/utils'; +import { Options } from 'cypress-axe'; + +const ITEM_EDIT_PAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('/edit'); + +beforeEach(() => { + // All tests start with visiting the Edit Item Page + cy.visit(ITEM_EDIT_PAGE); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); +}); + +describe('Edit Item > Edit Metadata tab', () => { + it('should pass accessibility tests', () => { + cy.get('a[data-test="metadata"]').should('be.visible'); + cy.get('a[data-test="metadata"]').click(); + + // Our selected tab should be both visible & active + cy.get('a[data-test="metadata"]').should('be.visible'); + cy.get('a[data-test="metadata"]').should('have.class', 'active'); + + // tag must be loaded + cy.get('ds-edit-item-page').should('be.visible'); + + // wait for all the ds-dso-edit-metadata-value components to be rendered + cy.get('ds-dso-edit-metadata-value div[role="row"]').each(($row: HTMLDivElement) => { + cy.wrap($row).find('div[role="cell"]').should('be.visible'); + }); + + // Analyze for accessibility issues + testA11y('ds-edit-item-page'); + }); +}); + +describe('Edit Item > Status tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="status"]').should('be.visible'); + cy.get('a[data-test="status"]').click(); + + // Our selected tab should be both visible & active + cy.get('a[data-test="status"]').should('be.visible'); + cy.get('a[data-test="status"]').should('have.class', 'active'); + + // tag must be loaded + cy.get('ds-item-status').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-status'); + }); +}); + +describe('Edit Item > Bitstreams tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="bitstreams"]').should('be.visible'); + cy.get('a[data-test="bitstreams"]').click(); + + // Our selected tab should be both visible & active + cy.get('a[data-test="bitstreams"]').should('be.visible'); + cy.get('a[data-test="bitstreams"]').should('have.class', 'active'); + + // tag must be loaded + cy.get('ds-item-bitstreams').should('be.visible'); + + // Table of item bitstreams must also be loaded + cy.get('div.item-bitstreams').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-bitstreams', + { + rules: { + // Currently Bitstreams page loads a pagination component per Bundle + // and they all use the same 'id="p-dad"'. + 'duplicate-id': { enabled: false }, + }, + } as Options, + ); + }); +}); + +describe('Edit Item > Curate tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="curate"]').should('be.visible'); + cy.get('a[data-test="curate"]').click(); + + // Our selected tab should be both visible & active + cy.get('a[data-test="curate"]').should('be.visible'); + cy.get('a[data-test="curate"]').should('have.class', 'active'); + + // tag must be loaded + cy.get('ds-item-curate').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-curate'); + }); +}); + +describe('Edit Item > Relationships tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="relationships"]').should('be.visible'); + cy.get('a[data-test="relationships"]').click(); + + // Our selected tab should be both visible & active + cy.get('a[data-test="relationships"]').should('be.visible'); + cy.get('a[data-test="relationships"]').should('have.class', 'active'); + + // tag must be loaded + cy.get('ds-item-relationships').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-relationships'); + }); +}); + +describe('Edit Item > Version History tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="versionhistory"]').should('be.visible'); + cy.get('a[data-test="versionhistory"]').click(); + + // Our selected tab should be both visible & active + cy.get('a[data-test="versionhistory"]').should('be.visible'); + cy.get('a[data-test="versionhistory"]').should('have.class', 'active'); + + // tag must be loaded + cy.get('ds-item-version-history').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-version-history'); + }); +}); + +describe('Edit Item > Access Control tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="access-control"]').should('be.visible'); + cy.get('a[data-test="access-control"]').click(); + + // Our selected tab should be both visible & active + cy.get('a[data-test="access-control"]').should('be.visible'); + cy.get('a[data-test="access-control"]').should('have.class', 'active'); + + // tag must be loaded + cy.get('ds-item-access-control').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-item-access-control'); + }); +}); + +describe('Edit Item > Collection Mapper tab', () => { + + it('should pass accessibility tests', () => { + cy.get('a[data-test="mapper"]').should('be.visible'); + cy.get('a[data-test="mapper"]').click(); + + // Our selected tab should be both visible & active + cy.get('a[data-test="mapper"]').should('be.visible'); + cy.get('a[data-test="mapper"]').should('have.class', 'active'); + + // tag must be loaded + cy.get('ds-item-collection-mapper').should('be.visible'); + + // Analyze entire page for accessibility issues + testA11y('ds-item-collection-mapper'); + + // Click on the "Map new collections" tab + cy.get('li[data-test="mapTab"] a').click(); + + // Make sure search form is now visible + cy.get('ds-search-form').should('be.visible'); + + // Analyze entire page (again) for accessibility issues + testA11y('ds-item-collection-mapper'); + }); +}); diff --git a/cypress/e2e/item-page.cy.ts b/cypress/e2e/item-page.cy.ts new file mode 100644 index 00000000000..0c434e85237 --- /dev/null +++ b/cypress/e2e/item-page.cy.ts @@ -0,0 +1,47 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Item Page', () => { + const ITEMPAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); + const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); + + // Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid] + it('should redirect to the entity page when navigating to an item page', () => { + cy.visit(ITEMPAGE); + cy.wait(1000); + cy.location('pathname').should('eq', ENTITYPAGE); + }); + + // CLARIN + // NOTE: accessibility tests are failing because the UI has been changed + // it('should pass accessibility tests', () => { + // cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent'); + // + // cy.visit(ENTITYPAGE); + // + // // Wait for the "viewevent" to trigger on the Item page. + // // This ensures our tag is fully loaded, as the tag is contained within it. + // cy.wait('@viewevent'); + // + // // tag must be loaded + // cy.get('ds-item-page').should('be.visible'); + // + // // Analyze for accessibility issues + // testA11y('ds-item-page'); + // }); + + // it('should pass accessibility tests on full item page', () => { + // cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent'); + // + // cy.visit(ENTITYPAGE + '/full'); + // + // // Wait for the "viewevent" to trigger on the Item page. + // // This ensures our tag is fully loaded, as the tag is contained within it. + // cy.wait('@viewevent'); + // + // // tag must be loaded + // cy.get('ds-full-item-page').should('be.visible'); + // + // // Analyze for accessibility issues + // testA11y('ds-full-item-page'); + // }); +}); diff --git a/cypress/e2e/item-statistics.cy.ts b/cypress/e2e/item-statistics.cy.ts new file mode 100644 index 00000000000..2537416d33f --- /dev/null +++ b/cypress/e2e/item-statistics.cy.ts @@ -0,0 +1,44 @@ +import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e'; +import { testA11y } from 'cypress/support/utils'; + +describe('Item Statistics Page', () => { + const ITEMSTATISTICSPAGE = '/statistics/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); + + // NOTE add statistics to the navbar and change this test + // it('should load if you click on "Statistics" from an Item/Entity page', () => { + // cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'))); + // cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click(); + // cy.location('pathname').should('eq', ITEMSTATISTICSPAGE); + // }); + + it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => { + cy.visit(ITEMSTATISTICSPAGE); + cy.get('ds-item-statistics-page').should('be.visible'); + cy.get('ds-item-page').should('not.exist'); + }); + + it('should contain a "Total visits" section', () => { + cy.visit(ITEMSTATISTICSPAGE); + cy.get('table[data-test="TotalVisits"]').should('be.visible'); + }); + + it('should contain a "Total visits per month" section', () => { + cy.visit(ITEMSTATISTICSPAGE); + cy.get('table[data-test="TotalVisitsPerMonth"]').should('be.visible'); + }); + + it('should pass accessibility tests', () => { + cy.visit(ITEMSTATISTICSPAGE); + + // tag must be loaded + cy.get('ds-item-statistics-page').should('be.visible'); + + // Verify / wait until "Total Visits" table's label is non-empty + // (This table loads these labels asynchronously, so we want to wait for them before analyzing page) + cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT); + + // TODO accessibility tests are failing because the UI has been changed + // Analyze for accessibility issues + // testA11y('ds-item-statistics-page'); + }); +}); diff --git a/cypress/e2e/login-modal.cy.ts b/cypress/e2e/login-modal.cy.ts new file mode 100644 index 00000000000..c7ba2d8197c --- /dev/null +++ b/cypress/e2e/login-modal.cy.ts @@ -0,0 +1,150 @@ +import { testA11y } from 'cypress/support/utils'; + +const page = { + openLoginMenu() { + // Click the "Log In" dropdown menu in header + cy.get('[data-test="login-menu"]').click(); + }, + openUserMenu() { + // Once logged in, click the User menu in header + cy.get('[data-test="user-menu"]').click(); + }, + submitLoginAndPasswordByPressingButton(email, password) { + // Enter email + cy.get('[data-test="email"]').type(email); + // Enter password + cy.get('[data-test="password"]').type(password); + // Click login button + cy.get('[data-test="login-button"]').click(); + }, + submitLoginAndPasswordByPressingEnter(email, password) { + // In opened Login modal, fill out email & password, then click Enter + cy.get('[data-test="email"]').type(email); + cy.get('[data-test="password"]').type(password); + cy.get('[data-test="password"]').type('{enter}'); + }, + submitLogoutByPressingButton() { + // This is the POST command that will actually log us out + cy.intercept('POST', '/server/api/authn/logout').as('logout'); + // Click logout button + cy.get('[data-test="logout-button"]').click(); + // Wait until above POST command responds before continuing + // (This ensures next action waits until logout completes) + cy.wait('@logout'); + }, +}; +// CLARIN - CLARIN-DSpace7.x has different login +// describe('Login Modal', () => { +// it('should login when clicking button & stay on same page', () => { +// const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')); +// cy.visit(ENTITYPAGE); +// +// // Login menu should exist +// cy.get('ds-log-in').should('exist'); +// +// // Login, and the tag should no longer exist +// page.openLoginMenu(); +// cy.get('.form-login').should('be.visible'); +// +// page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); +// cy.get('ds-log-in').should('not.exist'); +// +// // Verify we are still on the same page +// cy.url().should('include', ENTITYPAGE); +// +// // Open user menu, verify user menu & logout button now available +// page.openUserMenu(); +// cy.get('ds-user-menu').should('be.visible'); +// cy.get('ds-log-out').should('be.visible'); +// }); +// +// it('should login when clicking enter key & stay on same page', () => { +// cy.visit('/home'); +// +// // Open login menu in header & verify tag is visible +// page.openLoginMenu(); +// cy.get('.form-login').should('be.visible'); +// +// // Login, and the tag should no longer exist +// page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); +// cy.get('ds-log-in').should('not.exist'); +// +// // Verify we are still on homepage +// cy.url().should('include', '/home'); +// +// // Open user menu, verify user menu & logout button now available +// page.openUserMenu(); +// cy.get('ds-user-menu').should('be.visible'); +// cy.get('ds-log-out').should('be.visible'); +// }); +// +// it('should support logout', () => { +// // First authenticate & access homepage +// cy.login(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); +// cy.visit('/'); +// +// // Verify ds-log-in tag doesn't exist, but ds-log-out tag does exist +// cy.get('ds-log-in').should('not.exist'); +// cy.get('ds-log-out').should('exist'); +// +// // Click logout button +// page.openUserMenu(); +// page.submitLogoutByPressingButton(); +// +// // Verify ds-log-in tag now exists +// cy.get('ds-log-in').should('exist'); +// cy.get('ds-log-out').should('not.exist'); +// }); +// +// it('should allow new user registration', () => { +// cy.visit('/'); +// +// page.openLoginMenu(); +// +// // Registration link should be visible +// cy.get('ds-themed-header [data-test="register"]').should('be.visible'); +// +// // Click registration link & you should go to registration page +// cy.get('ds-themed-header [data-test="register"]').click(); +// cy.location('pathname').should('eq', '/register'); +// cy.get('ds-register-email').should('exist'); +// +// // Test accessibility of this page +// testA11y('ds-register-email'); +// }); +// +// it('should allow forgot password', () => { +// cy.visit('/'); +// +// page.openLoginMenu(); +// +// // Forgot password link should be visible +// cy.get('ds-themed-header [data-test="forgot"]').should('be.visible'); +// +// // Click link & you should go to Forgot Password page +// cy.get('ds-themed-header [data-test="forgot"]').click(); +// cy.location('pathname').should('eq', '/forgot'); +// cy.get('ds-forgot-email').should('exist'); +// +// // Test accessibility of this page +// testA11y('ds-forgot-email'); +// }); +// +// it('should pass accessibility tests in menus', () => { +// cy.visit('/'); +// +// // Open login menu & verify accessibility +// page.openLoginMenu(); +// cy.get('ds-log-in').should('exist'); +// testA11y('ds-log-in'); +// +// // Now login +// page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); +// cy.get('ds-log-in').should('not.exist'); +// +// // Open user menu, verify user menu accesibility +// page.openUserMenu(); +// cy.get('ds-user-menu').should('be.visible'); +// testA11y('ds-user-menu'); +// }); +// }); diff --git a/cypress/e2e/metadata-import-page.cy.ts b/cypress/e2e/metadata-import-page.cy.ts new file mode 100644 index 00000000000..a31c18e4ebb --- /dev/null +++ b/cypress/e2e/metadata-import-page.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Metadata Import Page', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/metadata-import'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Metadata import form must first be visible + cy.get('ds-metadata-import-page').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-metadata-import-page'); + }); +}); diff --git a/cypress/e2e/metadata-registry.cy.ts b/cypress/e2e/metadata-registry.cy.ts new file mode 100644 index 00000000000..0402d33153e --- /dev/null +++ b/cypress/e2e/metadata-registry.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Metadata Registry', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/registries/metadata'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Page must first be visible + cy.get('ds-metadata-registry').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-metadata-registry'); + }); +}); diff --git a/cypress/e2e/metadata-schema.cy.ts b/cypress/e2e/metadata-schema.cy.ts new file mode 100644 index 00000000000..9ff0db0714b --- /dev/null +++ b/cypress/e2e/metadata-schema.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Metadata Schema', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/registries/metadata/dc'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Page must first be visible + cy.get('ds-metadata-schema').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-metadata-schema'); + }); +}); diff --git a/cypress/e2e/my-dspace.cy.ts b/cypress/e2e/my-dspace.cy.ts new file mode 100644 index 00000000000..f43ed7dcff4 --- /dev/null +++ b/cypress/e2e/my-dspace.cy.ts @@ -0,0 +1,147 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('My DSpace page', () => { + it('should display recent submissions and pass accessibility tests', () => { + cy.visit('/mydspace'); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + + cy.get('ds-my-dspace-page').should('be.visible'); + + // CLARIN + // CLARIN-search component show only Items, so there are no records in the /mydspace page + // At least one recent submission should be displayed + // cy.get('[data-test="list-object"]').should('be.visible'); + // CLARIN + + // Click each filter toggle to open *every* filter + // (As we want to scan filter section for accessibility issues as well) + cy.get('.filter-toggle').click({ multiple: true }); + + // Analyze for accessibility issues + // CLARIN + // Commented out accessibility violations + // testA11y('ds-my-dspace-page'); + // CLARIN + }); + + it('should have a working detailed view that passes accessibility tests', () => { + cy.visit('/mydspace'); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + + cy.get('ds-my-dspace-page').should('be.visible'); + + // CLARIN + // This test was commented out because there are no options for a detailed view in the CLARIN-search component + // it is e.g., `Grid` or `List` view + // Click button in sidebar to display detailed view + // cy.get('ds-search-sidebar [data-test="detail-view"]').click(); + + // CLARIN-search component show only Items, so there are no records in the /mydspace page + // cy.get('ds-object-detail').should('be.visible'); + + // Analyze for accessibility issues + // testA11y('ds-my-dspace-page'); + }); + + // NOTE: Deleting existing submissions is exercised by submission.spec.ts + it('should let you start a new submission & edit in-progress submissions', () => { + cy.visit('/mydspace'); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + + // Open the New Submission dropdown + cy.get('button[data-test="submission-dropdown"]').click(); + // Click on the "Item" type in that dropdown + cy.get('#entityControlsDropdownMenu button[title="none"]').click(); + + // This should display the (popup window) + cy.get('ds-create-item-parent-selector').should('be.visible'); + + // Type in a known Collection name in the search box + cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')); + + // Click on the button matching that known Collection name + cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')).concat('"]')).click(); + + // New URL should include /workspaceitems, as we've started a new submission + cy.url().should('include', '/workspaceitems'); + + // The Submission edit form tag should be visible + cy.get('ds-submission-edit').should('be.visible'); + + // A Collection menu button should exist & its value should be the selected collection + cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')); + + // Now that we've created a submission, we'll test that we can go back and Edit it. + // Get our Submission URL, to parse out the ID of this new submission + cy.location().then(fullUrl => { + // This will be the full path (/workspaceitems/[id]/edit) + const path = fullUrl.pathname; + // Split on the slashes + const subpaths = path.split('/'); + // Part 2 will be the [id] of the submission + const id = subpaths[2]; + + // Click the "Save for Later" button to save this submission + cy.get('ds-submission-form-footer [data-test="save-for-later"]').click(); + + // "Save for Later" should send us to MyDSpace + cy.url().should('include', '/mydspace'); + + // Close any open notifications, to make sure they don't get in the way of next steps + cy.get('[data-dismiss="alert"]').click({multiple: true}); + + // This is the GET command that will actually run the search + cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); + // On MyDSpace, find the submission we just created via its ID + cy.get('[data-test="search-box"]').type(id); + cy.get('[data-test="search-button"]').click(); + + // Wait for search results to come back from the above GET command + cy.wait('@search-results'); + + // CLARIN + // CLARIN-search component show only Items, so there are no records in the /mydspace page + // Click the Edit button for this in-progress submission + // cy.get('#edit_' + id).click(); + + // Should send us back to the submission form + // cy.url().should('include', '/workspaceitems/' + id + '/edit'); + // + // // Discard our new submission by clicking Discard in Submission form & confirming + // cy.get('ds-submission-form-footer [data-test="discard"]').click(); + // cy.get('button#discard_submit').click(); + // + // // Discarding should send us back to MyDSpace + // cy.url().should('include', '/mydspace'); + // CLARIN + }); + }); + + it('should let you import from external sources', () => { + cy.visit('/mydspace'); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD')); + + // Open the New Import dropdown + cy.get('button[data-test="import-dropdown"]').click(); + // Click on the "Item" type in that dropdown + cy.get('#importControlsDropdownMenu button[title="none"]').click(); + + // New URL should include /import-external, as we've moved to the import page + cy.url().should('include', '/import-external'); + + // The external import searchbox should be visible + cy.get('ds-submission-import-external-searchbar').should('be.visible'); + + // Test for accessibility issues + testA11y('ds-submission-import-external'); + }); + +}); diff --git a/cypress/e2e/new-process.cy.ts b/cypress/e2e/new-process.cy.ts new file mode 100644 index 00000000000..d26da7cc4df --- /dev/null +++ b/cypress/e2e/new-process.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('New Process', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/processes/new'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Process form must first be visible + cy.get('ds-new-process').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-new-process'); + }); +}); diff --git a/cypress/e2e/pagenotfound.cy.ts b/cypress/e2e/pagenotfound.cy.ts new file mode 100644 index 00000000000..d02aa8541c3 --- /dev/null +++ b/cypress/e2e/pagenotfound.cy.ts @@ -0,0 +1,18 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('PageNotFound', () => { + it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => { + // request an invalid page (UUIDs at root path aren't valid) + cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false }); + cy.get('ds-pagenotfound').should('be.visible'); + + // Analyze for accessibility issues + testA11y('ds-pagenotfound'); + }); + + it('should not contain element ds-pagenotfound when navigating to existing page', () => { + cy.visit('/home'); + cy.get('ds-pagenotfound').should('not.exist'); + }); + +}); diff --git a/cypress/e2e/privacy.cy.ts b/cypress/e2e/privacy.cy.ts new file mode 100644 index 00000000000..16e049f701e --- /dev/null +++ b/cypress/e2e/privacy.cy.ts @@ -0,0 +1,13 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Privacy', () => { + it('should pass accessibility tests', () => { + cy.visit('/info/privacy'); + + // Page must first be visible + cy.get('ds-privacy').should('be.visible'); + + // Analyze for accessibility + testA11y('ds-privacy'); + }); +}); diff --git a/cypress/e2e/processes-overview.cy.ts b/cypress/e2e/processes-overview.cy.ts new file mode 100644 index 00000000000..2be3bd4c181 --- /dev/null +++ b/cypress/e2e/processes-overview.cy.ts @@ -0,0 +1,17 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Processes Overview', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/processes'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + + // Process overview must first be visible + cy.get('ds-process-overview').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-process-overview'); + }); +}); diff --git a/cypress/e2e/profile-page.cy.ts b/cypress/e2e/profile-page.cy.ts new file mode 100644 index 00000000000..911ef33ba58 --- /dev/null +++ b/cypress/e2e/profile-page.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('Profile page', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/profile'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Process form must first be visible + cy.get('ds-profile-page').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-profile-page'); + }); +}); diff --git a/cypress/e2e/search-navbar.cy.ts b/cypress/e2e/search-navbar.cy.ts new file mode 100644 index 00000000000..c06c3fd4f3b --- /dev/null +++ b/cypress/e2e/search-navbar.cy.ts @@ -0,0 +1,66 @@ +const page = { + fillOutQueryInNavBar(query) { + // Click the magnifying glass + cy.get('ds-themed-header [data-test="header-search-icon"]').click(); + // Fill out a query in input that appears + cy.get('ds-themed-header [data-test="header-search-box"]').type(query); + }, + submitQueryByPressingEnter() { + cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}'); + }, + submitQueryByPressingIcon() { + cy.get('ds-themed-header [data-test="header-search-icon"]').click(); + } +}; + +// CLARIN +// NOTE: search was removed from the navbar - these tests are not actual +// describe('Search from Navigation Bar', () => { +// // NOTE: these tests currently assume this query will return results! +// const query = Cypress.env('DSPACE_TEST_SEARCH_TERM'); +// +// it('should go to search page with correct query if submitted (from home)', () => { +// cy.visit('/'); +// // This is the GET command that will actually run the search +// cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); +// // Run the search +// page.fillOutQueryInNavBar(query); +// page.submitQueryByPressingEnter(); +// // New URL should include query param +// cy.url().should('include', 'query='.concat(query)); +// // Wait for search results to come back from the above GET command +// cy.wait('@search-results'); +// // At least one search result should be displayed +// cy.get('[data-test="list-object"]').should('be.visible'); +// }); +// +// it('should go to search page with correct query if submitted (from search)', () => { +// cy.visit('/search'); +// // This is the GET command that will actually run the search +// cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); +// // Run the search +// page.fillOutQueryInNavBar(query); +// page.submitQueryByPressingEnter(); +// // New URL should include query param +// cy.url().should('include', 'query='.concat(query)); +// // Wait for search results to come back from the above GET command +// cy.wait('@search-results'); +// // At least one search result should be displayed +// cy.get('[data-test="list-object"]').should('be.visible'); +// }); +// +// it('should allow user to also submit query by clicking icon', () => { +// cy.visit('/'); +// // This is the GET command that will actually run the search +// cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); +// // Run the search +// page.fillOutQueryInNavBar(query); +// page.submitQueryByPressingIcon(); +// // New URL should include query param +// cy.url().should('include', 'query='.concat(query)); +// // Wait for search results to come back from the above GET command +// cy.wait('@search-results'); +// // At least one search result should be displayed +// cy.get('[data-test="list-object"]').should('be.visible'); +// }); +// }); diff --git a/cypress/e2e/search-page.cy.ts b/cypress/e2e/search-page.cy.ts new file mode 100644 index 00000000000..dcefe96e832 --- /dev/null +++ b/cypress/e2e/search-page.cy.ts @@ -0,0 +1,59 @@ +import { Options } from 'cypress-axe'; +import { testA11y } from 'cypress/support/utils'; + +describe('Search Page', () => { + // NOTE: these tests currently assume this query will return results! + const query = Cypress.env('DSPACE_TEST_SEARCH_TERM'); + + it('should redirect to the correct url when query was set and submit button was triggered', () => { + const queryString = 'Another interesting query string'; + cy.visit('/search'); + // Type query in searchbox & click search button + cy.get('[data-test="search-box"]').type(queryString); + cy.get('[data-test="search-button"]').click(); + cy.url().should('include', 'query=' + encodeURI(queryString)); + }); + + // CLARIN + // NOTE: accessibility tests are failing because the UI has been changed + // it('should load results and pass accessibility tests', () => { + // cy.visit('/search?query='.concat(query)); + // cy.get('[data-test="search-box"]').should('have.value', query); + // + // // tag must be loaded + // cy.get('ds-search-page').should('be.visible'); + // + // // At least one search result should be displayed + // cy.get('[data-test="list-object"]').should('be.visible'); + // + // // Click each filter toggle to open *every* filter + // // (As we want to scan filter section for accessibility issues as well) + // cy.get('[data-test="filter-toggle"]').click({ multiple: true }); + // + // // Analyze for accessibility issues + // testA11y('ds-search-page'); + // }); + // + // it('should have a working grid view that passes accessibility tests', () => { + // cy.visit('/search?query='.concat(query)); + // + // // Click button in sidebar to display grid view + // cy.get('ds-search-sidebar [data-test="grid-view"]').click(); + // + // // tag must be loaded + // cy.get('ds-search-page').should('be.visible'); + // + // // At least one grid object (card) should be displayed + // cy.get('[data-test="grid-object"]').should('be.visible'); + // + // // Analyze for accessibility issues + // testA11y('ds-search-page', + // { + // rules: { + // // Card titles fail this test currently + // 'heading-order': { enabled: false } + // } + // } as Options + // ); + // }); +}); diff --git a/cypress/e2e/submission-ui.cy.ts b/cypress/e2e/submission-ui.cy.ts new file mode 100644 index 00000000000..1b6f9b3ca3b --- /dev/null +++ b/cypress/e2e/submission-ui.cy.ts @@ -0,0 +1,339 @@ +/** + * This IT will be never be pushed to the upstream because clicking testing DOM elements is antipattern because + * the tests on other machines could fail. + */ +import { + TEST_ADMIN_PASSWORD, + TEST_ADMIN_USER, + TEST_SUBMIT_CLARIAH_COLLECTION_UUID, + TEST_SUBMIT_COLLECTION_UUID +} from '../support/e2e'; +import { createItemProcess } from '../support/commands'; + + +const sideBarMenu = { + clickOnNewButton() { + cy.get('.sidebar-top-level-items div[role = "button"]').eq(0).click(); + }, + clickOnNewCommunityButton() { + cy.get('.sidebar-sub-level-items a[role = "button"]').eq(0).click(); + }, + clickOnNewCollectionButton() { + cy.get('.sidebar-sub-level-items a[role = "button"]').eq(1).click(); + }, + clickOnNewItemButton() { + cy.get('.sidebar-sub-level-items a[role = "button"]').eq(2).click(); + } +}; + +describe('Create a new submission', () => { + beforeEach(() => { + // Create a new submission + cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + }); + + // Test openAIRE - configured more retries because it failed with 3 retries + // Note: openAIRE tests are commented because they are failing in the server but locally they success. + // it('should add non EU sponsor without suggestion', { + // retries: { + // runMode: 6, + // openMode: 6, + // }, + // },() => { + // // funding code + // cy.get('ds-dynamic-sponsor-autocomplete').eq(0).click({force: true}).type('code'); + // // suggestion is popped up - must blur + // cy.get('body').click(0,0); + // cy.wait(250); + // // local.sponsor_COMPLEX_INPUT_3 + // cy.get('ds-dynamic-sponsor-autocomplete').eq(1).click({force: true}).type('projectName'); + // // blur because after each click on input will send PATCH request and the input value is removed + // cy.get('body').click(0,0); + // cy.wait(250); + // // select sponsor type + // createItemProcess.clickOnSelectionInput('local.sponsor_COMPLEX_INPUT_0'); + // createItemProcess.clickOnSelection('N/A',0); + // cy.wait(250); + // // sponsor organisation + // createItemProcess.writeValueToInput('local.sponsor_COMPLEX_INPUT_2', 'organisation', false); + // }); + // + // it('should load and add EU sponsor from suggestion',{ + // retries: { + // runMode: 6, + // openMode: 6, + // }, + // }, () => { + // // select sponsor type + // createItemProcess.clickOnSelectionInput('local.sponsor_COMPLEX_INPUT_0'); + // createItemProcess.clickOnSelection('EU',0); + // cy.wait(250); + // // write suggestion for the eu sponsor - local.sponsor_COMPLEX_INPUT_1 + // cy.get('ds-dynamic-sponsor-autocomplete').eq(0).click({force: true}).type('eve'); + // // select suggestion + // createItemProcess.clickOnSuggestionSelection(0); + // cy.wait(250); + // // EU input field should be visible + // createItemProcess.checkIsInputVisible('local.sponsor_COMPLEX_INPUT_4'); + // }); + // + // it('should add four EU sponsors', { + // retries: { + // runMode: 6, + // openMode: 6, + // }, + // },() => { + // // select sponsor type + // createItemProcess.clickOnSelectionInput('local.sponsor_COMPLEX_INPUT_0'); + // createItemProcess.clickOnSelection('EU',0); + // cy.wait(250); + // // write suggestion for the eu sponsor - local.sponsor_COMPLEX_INPUT_1 + // cy.get('ds-dynamic-sponsor-autocomplete').eq(0).click({force: true}).type('eve'); + // // select suggestion + // createItemProcess.clickOnSuggestionSelection(0); + // cy.wait(250); + // // EU input field should be visible + // createItemProcess.checkIsInputVisible('local.sponsor_COMPLEX_INPUT_4'); + // + // // add another sponsors + // addEUSponsor(1); + // addEUSponsor(2); + // addEUSponsor(3); + // }); + + // Test type-bind + it('should be showed chosen type value', { + retries: { + runMode: 6, + openMode: 6, + }, + defaultCommandTimeout: 10000 + },() => { + createItemProcess.clickOnSelectionInput('dc.type'); + createItemProcess.clickOnTypeSelection('Corpus'); + }); + + // Test CMDI input field + it('should be visible Has CMDI file input field because user is admin', { + retries: { + runMode: 6, + openMode: 6, + }, + defaultCommandTimeout: 10000 + },() => { + createItemProcess.checkLocalHasCMDIVisibility(); + }); + + it('The local.hasCMDI value should be sent in the response after type change', { + retries: { + runMode: 6, + openMode: 6, + }, + defaultCommandTimeout: 10000 + },() => { + createItemProcess.clickOnSelectionInput('dc.type'); + createItemProcess.clickOnTypeSelection('Corpus'); + // Wait because after the type change, the `Save` request is sent, and the page is reloaded. + // The checkbox could be checked during the reloading process. + cy.wait(500); + createItemProcess.checkCheckbox('local_hasCMDI'); + createItemProcess.controlCheckedCheckbox('local_hasCMDI',true); + createItemProcess.clickOnSave(); + cy.reload(); + createItemProcess.controlCheckedCheckbox('local_hasCMDI',true); + }); + + it('should change the step status after accepting/declining the distribution license', { + retries: { + runMode: 6, + openMode: 6, + }, + defaultCommandTimeout: 10000 + },() => { + createItemProcess.checkDistributionLicenseStep(); + createItemProcess.checkDistributionLicenseToggle(); + // default status value is warnings + createItemProcess.checkDistributionLicenseStatus('Warnings'); + // accept the distribution license agreement + createItemProcess.clickOnDistributionLicenseToggle(); + // after accepting the status should be valid + createItemProcess.checkDistributionLicenseStatus('Valid'); + // click on the toggle again and status should be changed to `Warnings` + createItemProcess.clickOnDistributionLicenseToggle(); + createItemProcess.checkDistributionLicenseStatus('Warnings'); + }); + + it('should pick up the license from the license selector', { + retries: { + runMode: 6, + openMode: 6, + }, + defaultCommandTimeout: 10000 + },() => { + createItemProcess.checkLicenseResourceStep(); + // check default value in the license dropdown selection + createItemProcess.checkLicenseSelectionValue('Select a License ...'); + // pop up the license selector modal + createItemProcess.clickOnLicenseSelectorButton(); + // check if the modal was popped up + createItemProcess.checkLicenseSelectorModal(); + // pick up the first license from the modal, it is `Public Domain Mark (PD)` + createItemProcess.pickUpLicenseFromLicenseSelector(); + // check if the picked up license value is seen as selected value in the selection + createItemProcess.checkLicenseSelectionValue('Public Domain Mark (PD)'); + }); + + it.skip('should select the license from the license selection dropdown and change status', { + retries: { + runMode: 6, + openMode: 6, + }, + defaultCommandTimeout: 10000 + },() => { + createItemProcess.checkLicenseResourceStep(); + + // check default value in the license dropdown selection + createItemProcess.checkLicenseSelectionValue('Select a License ...'); + // check step status - it should be as warning + createItemProcess.checkResourceLicenseStatus('Warnings'); + // click on the dropdown button to list options + createItemProcess.clickOnLicenseSelectionButton(); + // select `Public Domain Mark (PD)` from the selection + createItemProcess.selectValueFromLicenseSelection(2); + // // selected value should be seen as selected value in the selection + createItemProcess.checkLicenseSelectionValue('GNU General Public License, version 2'); + // // check step status - it should be valid + createItemProcess.checkResourceLicenseStatus('Valid'); + }); + + it.skip('should show warning messages if was selected non-supported license', { + retries: { + runMode: 6, + openMode: 6, + }, + defaultCommandTimeout: 10000 + },() => { + createItemProcess.checkLicenseResourceStep(); + + // check default value in the license dropdown selection + createItemProcess.checkLicenseSelectionValue('Select a License ...'); + // check step status - it should be as warning + createItemProcess.checkResourceLicenseStatus('Warnings'); + // click on the dropdown button to list options + createItemProcess.clickOnLicenseSelectionButton(); + // select `Select a License ...` from the selection - this license is not supported + createItemProcess.selectValueFromLicenseSelection(0); + // selected value should be seen as selected value in the selection + createItemProcess.checkLicenseSelectionValue('Select a License ...'); + // check step status - it should an error + createItemProcess.checkResourceLicenseStatus('Errors'); + // error messages should be popped up + createItemProcess.showErrorMustChooseLicense(); + createItemProcess.showErrorNotSupportedLicense(); + }); + + it('should require license validation when file is uploaded', { + retries: { + runMode: 6, + openMode: 6, + }, + defaultCommandTimeout: 10000 + }, () => { + // Navigate to license step + createItemProcess.checkLicenseResourceStep(); + + // Upload a file to trigger license requirement + cy.get('ds-uploader').trigger('dragover'); + cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload'); + cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.svg', { + action: 'drag-drop' + }); + cy.wait('@upload'); + + // Allow frontend to propagate validation state to the header icon + cy.wait(1000); + cy.get('div[id="section_clarin-license"]').find('.card-header').should('be.visible'); + + // Now license is required - should see warnings + createItemProcess.checkLicenseSelectionValue('Select a License ...'); + createItemProcess.checkResourceLicenseStatus('Errors'); + + // Select a valid license + createItemProcess.clickOnLicenseSelectionButton(); + createItemProcess.selectValueFromLicenseSelection(2); // GNU GPL v2 + createItemProcess.checkLicenseSelectionValue('GNU General Public License, version 2'); + + // Status should change to Valid + createItemProcess.checkResourceLicenseStatus('Valid'); + }); + + it('should not show validation warnings when no file is uploaded (metadata-only)', { + retries: { + runMode: 6, + openMode: 6, + }, + defaultCommandTimeout: 10000 + }, () => { + // Navigate to license step + createItemProcess.checkLicenseResourceStep(); + + // DON'T upload any file - just check the license section is accessible + createItemProcess.checkLicenseSelectionValue('Select a License ...'); + + // Verify warning and error icons do NOT exist + cy.get('div[id="section_clarin-license"]') + .find('.card-header') + .find('.fa-exclamation-circle.text-warning') + .should('not.exist'); + + cy.get('div[id="section_clarin-license"]') + .find('.card-header') + .find('.fa-exclamation-circle.text-danger') + .should('not.exist'); + + cy.get('div[id="section_clarin-license"]') + .find('.card-header') + .find('.fa-check-circle.text-success') + .should('be.visible'); + }); + + it('The submission should not have the Notice Step', { + retries: { + runMode: 6, + openMode: 6, + }, + defaultCommandTimeout: 10000 + },() => { + createItemProcess.checkClarinNoticeStepNotExist(); + }); +}); + +describe('Create a new submission in the clariah collection', () => { + beforeEach(() => { + // Create a new submission + cy.visit('/submit?collection=' + TEST_SUBMIT_CLARIAH_COLLECTION_UUID + '&entityType=none'); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + }); +}); + +function addEUSponsor(euSponsorOrder) { + createItemProcess.clickAddMore(1); + // select sponsor type of second sponsor + createItemProcess.clickOnSelectionInput('local.sponsor_COMPLEX_INPUT_0', euSponsorOrder); + createItemProcess.clickOnSelection('EU',euSponsorOrder); + cy.wait(500); + // write suggestion for the eu sponsor + // createItemProcess.writeValueToInput('local.sponsor_COMPLEX_INPUT_1', 'eve', true, euSponsorOrder); + // euSponsorOrder * 2 because sponsor complex type has two ds-dynamic-sponsor-autocomplete inputs + cy.get('ds-dynamic-sponsor-autocomplete').eq(euSponsorOrder * 2).click({force: true}).type('eve'); + // select suggestion + createItemProcess.clickOnSuggestionSelection(euSponsorOrder * 2); + cy.wait(250); + // EU input field should be visible + createItemProcess.checkIsInputVisible('local.sponsor_COMPLEX_INPUT_4', false, euSponsorOrder); +} diff --git a/cypress/e2e/submission.cy.ts b/cypress/e2e/submission.cy.ts new file mode 100644 index 00000000000..474d7c6cf2b --- /dev/null +++ b/cypress/e2e/submission.cy.ts @@ -0,0 +1,166 @@ +//import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID, TEST_ADMIN_USER, TEST_ADMIN_PASSWORD } from 'cypress/support/e2e'; +import { createItemProcess } from '../support/commands'; + +describe('New Submission page', () => { + + // NOTE: We already test that new Item submissions can be started from MyDSpace in my-dspace.spec.ts + it('should create a new submission when using /submit path & pass accessibility', () => { + // Test that calling /submit with collection & entityType will create a new submission + cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none')); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + + // Should redirect to /workspaceitems, as we've started a new submission + cy.url().should('include', '/workspaceitems'); + + // The Submission edit form tag should be visible + cy.get('ds-submission-edit').should('be.visible'); + + // A Collection menu button should exist & it's value should be the selected collection + cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')); + + // 4 sections should be visible by default + cy.get('div#section_traditionalpageone').should('be.visible'); + cy.get('div#section_traditionalpagetwo').should('be.visible'); + cy.get('div#section_upload').should('be.visible'); + cy.get('div#section_license').should('be.visible'); + + // Test entire page for accessibility + // CLARIN-DSpace still has some accessibility issues, so we will not fail the test + // testA11y('ds-submission-edit', + // { + // rules: { + // // Author & Subject fields have invalid "aria-multiline" attrs. + // // See https://github.com/DSpace/dspace-angular/issues/1272 + // 'aria-allowed-attr': { enabled: false }, + // // All panels are accordians & fail "aria-required-children" and "nested-interactive". + // // Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216 + // 'aria-required-children': { enabled: false }, + // 'nested-interactive': { enabled: false }, + // // All select boxes fail to have a name / aria-label. + // // This is a bug in ng-dynamic-forms and may require https://github.com/DSpace/dspace-angular/issues/2216 + // 'select-name': { enabled: false }, + // } + // + // } as Options + // ); + + // Discard button should work + // Clicking it will display a confirmation, which we will confirm with another click + cy.get('button#discard').click(); + cy.get('button#discard_submit').click(); + }); + + it('should block submission & show errors if required fields are missing', () => { + // Create a new submission + cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none')); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + + // Attempt an immediate deposit without filling out any fields + cy.get('button#deposit').click(); + + // A warning alert should display. + cy.get('ds-notification div.alert-success').should('not.exist'); + cy.get('ds-notification div.alert-warning').should('be.visible'); + + // First section should have an exclamation error in the header + // (as it has required fields) + cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible'); + + // Title field should have class "is-invalid" applied, as it's required + cy.get('input#dc_title').should('have.class', 'is-invalid'); + + // Date Year field should also have "is-valid" class + cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid'); + + // FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button. + // Get our Submission URL, to parse out the ID of this submission + cy.location().then(fullUrl => { + // This will be the full path (/workspaceitems/[id]/edit) + const path = fullUrl.pathname; + // Split on the slashes + const subpaths = path.split('/'); + // Part 2 will be the [id] of the submission + const id = subpaths[2]; + + // Even though form is incomplete, the "Save for Later" button should still work + cy.get('button#saveForLater').click(); + + // "Save for Later" should send us to MyDSpace + cy.url().should('include', '/mydspace'); + + // CLARIN + // // A success alert should be visible + // cy.get('ds-notification div.alert-success').should('be.visible'); + // // Now, dismiss any open alert boxes (may be multiple, as tests run quickly) + // cy.get('[data-dismiss="alert"]').click({multiple: true}); + // + // // This is the GET command that will actually run the search + // cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); + // // On MyDSpace, find the submission we just saved via its ID + // cy.get('[data-test="search-box"]').type(id); + // cy.get('[data-test="search-button"]').click(); + // + // // Wait for search results to come back from the above GET command + // cy.wait('@search-results'); + // + // // Delete our created submission & confirm deletion + // cy.get('button#delete_' + id).click(); + // cy.get('button#delete_confirm').click(); + }); + }); + + it('should allow for deposit if all required fields completed & file uploaded', () => { + // Create a new submission + cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none')); + + // This page is restricted, so we will be shown the login form. Fill it out & submit. + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + + // Fill out all required fields (Title, Date) + cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests'); + cy.get('input#dc_date_issued_year').type('2022'); + + // Confirm the required license by checking checkbox + // (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own ) + // CLARIN + createItemProcess.clickOnDistributionLicenseToggle(); + // click on the dropdown button to list options + createItemProcess.clickOnLicenseSelectionButton(); + // select `Public Domain Mark (PD)` from the selection + createItemProcess.selectValueFromLicenseSelection(2); + // // selected value should be seen as selected value in the selection + createItemProcess.checkLicenseSelectionValue('GNU General Public License, version 2'); + // CLARIN + + // Before using Cypress drag & drop, we have to manually trigger the "dragover" event. + // This ensures our UI displays the dropzone that covers the entire submission page. + // (For some reason Cypress drag & drop doesn't trigger this even itself & upload won't work without this trigger) + cy.get('ds-uploader').trigger('dragover'); + + // This is the POST command that will upload the file + cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload'); + + // Upload our DSpace logo via drag & drop onto submission form + // cy.get('div#section_upload') + cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.svg', { + action: 'drag-drop' + }); + + // Wait for upload to complete before proceeding + cy.wait('@upload'); + + // CLARIN + // // Wait for deposit button to not be disabled & click it. + // cy.get('button#deposit').should('not.be.disabled').click(); + // + // // No warnings should exist. Instead, just successful deposit alert is displayed + // cy.get('ds-notification div.alert-warning').should('not.exist'); + // cy.get('ds-notification div.alert-success').should('be.visible'); + // CLARIN + }); + +}); diff --git a/cypress/e2e/system-wide-alert.cy.ts b/cypress/e2e/system-wide-alert.cy.ts new file mode 100644 index 00000000000..046bfe619fe --- /dev/null +++ b/cypress/e2e/system-wide-alert.cy.ts @@ -0,0 +1,16 @@ +import { testA11y } from 'cypress/support/utils'; + +describe('System Wide Alert', () => { + beforeEach(() => { + // Must login as an Admin to see the page + cy.visit('/admin/system-wide-alert'); + cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD')); + }); + + it('should pass accessibility tests', () => { + // Page must first be visible + cy.get('ds-system-wide-alert-form').should('be.visible'); + // Analyze for accessibility issues + testA11y('ds-system-wide-alert-form'); + }); +}); diff --git a/cypress/e2e/tombstone.cy.ts b/cypress/e2e/tombstone.cy.ts new file mode 100644 index 00000000000..061b0ab9033 --- /dev/null +++ b/cypress/e2e/tombstone.cy.ts @@ -0,0 +1,94 @@ +import { + TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, + TEST_WITHDRAWN_ITEM, + TEST_WITHDRAWN_ITEM_WITH_REASON, + TEST_WITHDRAWN_ITEM_WITH_REASON_AND_AUTHORS, + TEST_WITHDRAWN_REPLACED_ITEM, TEST_WITHDRAWN_REPLACED_ITEM_WITH_AUTHORS +} from '../support/e2e'; + +const ITEMPAGE_WITHDRAWN = '/items/' + TEST_WITHDRAWN_ITEM; +const ITEMPAGE_WITHDRAWN_REASON = '/items/' + TEST_WITHDRAWN_ITEM_WITH_REASON; +const ITEMPAGE_WITHDRAWN_REPLACED = '/items/' + TEST_WITHDRAWN_REPLACED_ITEM; +const ITEMPAGE_WITHDRAWN_REASON_AUTHORS = '/items/' + TEST_WITHDRAWN_ITEM_WITH_REASON_AND_AUTHORS; +const ITEMPAGE_WITHDRAWN_REPLACED_AUTHORS = '/items/' + TEST_WITHDRAWN_REPLACED_ITEM_WITH_AUTHORS; +const TOMBSTONED_ITEM_MESSAGE = 'This item has been withdrawn'; + +// describe('Tombstone Page', () => { +// +// it('should see the items page the item must exists', () => { +// cy.visit(ITEMPAGE_WITHDRAWN); +// // tag must be loaded +// cy.get('ds-item-page').should('exist'); +// +// cy.visit(ITEMPAGE_WITHDRAWN_REASON); +// // tag must be loaded +// cy.get('ds-item-page').should('exist'); +// +// cy.visit(ITEMPAGE_WITHDRAWN_REPLACED); +// // tag must be loaded +// cy.get('ds-item-page').should('exist'); +// }); +// +// it('the user should see withdrawn tombstone', () => { +// cy.visit(ITEMPAGE_WITHDRAWN); +// cy.get('ds-withdrawn-tombstone').should('exist'); +// cy.get('ds-replaced-tombstone').should('not.exist'); +// cy.get('ds-view-tracker').should('not.exist'); +// }); +// +// it('the user should see withdrawn tombstone with the reason', () => { +// cy.visit(ITEMPAGE_WITHDRAWN_REASON); +// cy.get('ds-withdrawn-tombstone').contains(TEST_WITHDRAWN_REASON); +// }); +// +// it('the user should see replacement tombstone with the new destination', () => { +// cy.visit(ITEMPAGE_WITHDRAWN_REPLACED); +// cy.get('ds-replaced-tombstone').contains(TEST_WITHDRAWN_REPLACEMENT); +// }); +// +// it('the user should see withdrawn tombstone with the reason and with authors', () => { +// cy.visit(ITEMPAGE_WITHDRAWN_REASON_AUTHORS); +// cy.get('ds-withdrawn-tombstone').contains(TEST_WITHDRAWN_AUTHORS); +// }); +// +// it('the user should see replacement tombstone with the new destination and with the authors', () => { +// cy.visit(ITEMPAGE_WITHDRAWN_REPLACED_AUTHORS); +// cy.get('ds-replaced-tombstone').contains(TEST_WITHDRAWN_AUTHORS); +// }); +// +// }); + +describe('Admin Tombstone Page', () => { + beforeEach(() => { + cy.visit('/login'); + // Cancel discojuice login - only if it is popped up + cy.wait(500); + cy.get('.discojuice_close').should('exist').click(); + // Login as admin + cy.loginViaForm(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); + cy.visit('/'); + }); + + it('the admin should see ds-item-page',{ + retries: { + runMode: 8, + openMode: 8, + }, + defaultCommandTimeout: 10000 + }, () => { + cy.visit(ITEMPAGE_WITHDRAWN); + cy.get('ds-item-page').should('exist'); + }); + + it('the admin should see the withdrawn message on the replaced item', { + retries: { + runMode: 8, + openMode: 8, + }, + defaultCommandTimeout: 10000 + }, () => { + cy.visit(ITEMPAGE_WITHDRAWN_REPLACED); + cy.get('ds-item-page').contains(TOMBSTONED_ITEM_MESSAGE); + }); + +}); diff --git a/cypress/integration/breadcrumbs.spec.ts b/cypress/integration/breadcrumbs.spec.ts deleted file mode 100644 index 62b9a8ad1d3..00000000000 --- a/cypress/integration/breadcrumbs.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { TEST_ENTITY_PUBLICATION } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -describe('Breadcrumbs', () => { - it('should pass accessibility tests', () => { - // Visit an Item, as those have more breadcrumbs - cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION); - - // Wait for breadcrumbs to be visible - cy.get('ds-breadcrumbs').should('be.visible'); - - // Analyze for accessibility - testA11y('ds-breadcrumbs'); - }); -}); diff --git a/cypress/integration/browse-by-author.spec.ts b/cypress/integration/browse-by-author.spec.ts deleted file mode 100644 index 07c20ad7c91..00000000000 --- a/cypress/integration/browse-by-author.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { testA11y } from 'cypress/support/utils'; - -describe('Browse By Author', () => { - it('should pass accessibility tests', () => { - cy.visit('/browse/author'); - - // Wait for to be visible - cy.get('ds-browse-by-metadata-page').should('be.visible'); - - // Analyze for accessibility - testA11y('ds-browse-by-metadata-page'); - }); -}); diff --git a/cypress/integration/browse-by-dateissued.spec.ts b/cypress/integration/browse-by-dateissued.spec.ts deleted file mode 100644 index 4d22420227c..00000000000 --- a/cypress/integration/browse-by-dateissued.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { testA11y } from 'cypress/support/utils'; - -describe('Browse By Date Issued', () => { - it('should pass accessibility tests', () => { - cy.visit('/browse/dateissued'); - - // Wait for to be visible - cy.get('ds-browse-by-date-page').should('be.visible'); - - // Analyze for accessibility - testA11y('ds-browse-by-date-page'); - }); -}); diff --git a/cypress/integration/browse-by-subject.spec.ts b/cypress/integration/browse-by-subject.spec.ts deleted file mode 100644 index 89b791f03c4..00000000000 --- a/cypress/integration/browse-by-subject.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { testA11y } from 'cypress/support/utils'; - -describe('Browse By Subject', () => { - it('should pass accessibility tests', () => { - cy.visit('/browse/subject'); - - // Wait for to be visible - cy.get('ds-browse-by-metadata-page').should('be.visible'); - - // Analyze for accessibility - testA11y('ds-browse-by-metadata-page'); - }); -}); diff --git a/cypress/integration/browse-by-title.spec.ts b/cypress/integration/browse-by-title.spec.ts deleted file mode 100644 index e4e027586a8..00000000000 --- a/cypress/integration/browse-by-title.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { testA11y } from 'cypress/support/utils'; - -describe('Browse By Title', () => { - it('should pass accessibility tests', () => { - cy.visit('/browse/title'); - - // Wait for to be visible - cy.get('ds-browse-by-title-page').should('be.visible'); - - // Analyze for accessibility - testA11y('ds-browse-by-title-page'); - }); -}); diff --git a/cypress/integration/collection-page.spec.ts b/cypress/integration/collection-page.spec.ts deleted file mode 100644 index a0140d8faf2..00000000000 --- a/cypress/integration/collection-page.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { TEST_COLLECTION } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -describe('Collection Page', () => { - - it('should pass accessibility tests', () => { - cy.visit('/collections/' + TEST_COLLECTION); - - // tag must be loaded - cy.get('ds-collection-page').should('exist'); - - // Analyze for accessibility issues - testA11y('ds-collection-page'); - }); -}); diff --git a/cypress/integration/collection-statistics.spec.ts b/cypress/integration/collection-statistics.spec.ts deleted file mode 100644 index 90b569c8245..00000000000 --- a/cypress/integration/collection-statistics.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TEST_COLLECTION } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -describe('Collection Statistics Page', () => { - const COLLECTIONSTATISTICSPAGE = '/statistics/collections/' + TEST_COLLECTION; - - it('should load if you click on "Statistics" from a Collection page', () => { - cy.visit('/collections/' + TEST_COLLECTION); - cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); - cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE); - }); - - it('should contain a "Total visits" section', () => { - cy.visit(COLLECTIONSTATISTICSPAGE); - cy.get('.' + TEST_COLLECTION + '_TotalVisits').should('exist'); - }); - - it('should contain a "Total visits per month" section', () => { - cy.visit(COLLECTIONSTATISTICSPAGE); - cy.get('.' + TEST_COLLECTION + '_TotalVisitsPerMonth').should('exist'); - }); - - it('should pass accessibility tests', () => { - cy.visit(COLLECTIONSTATISTICSPAGE); - - // tag must be loaded - cy.get('ds-collection-statistics-page').should('exist'); - - // Analyze for accessibility issues - testA11y('ds-collection-statistics-page'); - }); -}); diff --git a/cypress/integration/community-list.spec.ts b/cypress/integration/community-list.spec.ts deleted file mode 100644 index a7ba72b74a1..00000000000 --- a/cypress/integration/community-list.spec.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Options } from 'cypress-axe'; -import { testA11y } from 'cypress/support/utils'; - -describe('Community List Page', () => { - - it('should pass accessibility tests', () => { - cy.visit('/community-list'); - - // tag must be loaded - cy.get('ds-community-list-page').should('exist'); - - // Open first Community (to show Collections)...that way we scan sub-elements as well - cy.get('ds-community-list :nth-child(1) > .btn-group > .btn').click(); - - // Analyze for accessibility issues - // Disable heading-order checks until it is fixed - testA11y('ds-community-list-page', - { - rules: { - 'heading-order': { enabled: false } - } - } as Options - ); - }); -}); diff --git a/cypress/integration/community-page.spec.ts b/cypress/integration/community-page.spec.ts deleted file mode 100644 index 79e21431ad3..00000000000 --- a/cypress/integration/community-page.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { TEST_COMMUNITY } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -describe('Community Page', () => { - - it('should pass accessibility tests', () => { - cy.visit('/communities/' + TEST_COMMUNITY); - - // tag must be loaded - cy.get('ds-community-page').should('exist'); - - // Analyze for accessibility issues - testA11y('ds-community-page',); - }); -}); diff --git a/cypress/integration/community-statistics.spec.ts b/cypress/integration/community-statistics.spec.ts deleted file mode 100644 index cbf1783c0b4..00000000000 --- a/cypress/integration/community-statistics.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { TEST_COMMUNITY } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -describe('Community Statistics Page', () => { - const COMMUNITYSTATISTICSPAGE = '/statistics/communities/' + TEST_COMMUNITY; - - it('should load if you click on "Statistics" from a Community page', () => { - cy.visit('/communities/' + TEST_COMMUNITY); - cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); - cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE); - }); - - it('should contain a "Total visits" section', () => { - cy.visit(COMMUNITYSTATISTICSPAGE); - cy.get('.' + TEST_COMMUNITY + '_TotalVisits').should('exist'); - }); - - it('should contain a "Total visits per month" section', () => { - cy.visit(COMMUNITYSTATISTICSPAGE); - cy.get('.' + TEST_COMMUNITY + '_TotalVisitsPerMonth').should('exist'); - }); - - it('should pass accessibility tests', () => { - cy.visit(COMMUNITYSTATISTICSPAGE); - - // tag must be loaded - cy.get('ds-community-statistics-page').should('exist'); - - // Analyze for accessibility issues - testA11y('ds-community-statistics-page'); - }); -}); diff --git a/cypress/integration/footer.spec.ts b/cypress/integration/footer.spec.ts deleted file mode 100644 index 656e9d47012..00000000000 --- a/cypress/integration/footer.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { testA11y } from 'cypress/support/utils'; - -describe('Footer', () => { - it('should pass accessibility tests', () => { - cy.visit('/'); - - // Footer must first be visible - cy.get('ds-footer').should('be.visible'); - - // Analyze for accessibility - testA11y('ds-footer'); - }); -}); diff --git a/cypress/integration/header.spec.ts b/cypress/integration/header.spec.ts deleted file mode 100644 index 236208db686..00000000000 --- a/cypress/integration/header.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { testA11y } from 'cypress/support/utils'; - -describe('Header', () => { - it('should pass accessibility tests', () => { - cy.visit('/'); - - // Header must first be visible - cy.get('ds-header').should('be.visible'); - - // Analyze for accessibility - testA11y({ - include: ['ds-header'], - exclude: [ - ['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174 - ['.dropdownLogin'] // "Log in" link has color contrast issues. Will be fixed in #1149 - ], - }); - }); -}); diff --git a/cypress/integration/homepage-statistics.spec.ts b/cypress/integration/homepage-statistics.spec.ts deleted file mode 100644 index fe0311f87ef..00000000000 --- a/cypress/integration/homepage-statistics.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { testA11y } from 'cypress/support/utils'; - -describe('Site Statistics Page', () => { - it('should load if you click on "Statistics" from homepage', () => { - cy.visit('/'); - cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); - cy.location('pathname').should('eq', '/statistics'); - }); - - it('should pass accessibility tests', () => { - cy.visit('/statistics'); - - // tag must be loaded - cy.get('ds-site-statistics-page').should('exist'); - - // Analyze for accessibility issues - testA11y('ds-site-statistics-page'); - }); -}); diff --git a/cypress/integration/homepage.spec.ts b/cypress/integration/homepage.spec.ts deleted file mode 100644 index 8fdf61dbf7f..00000000000 --- a/cypress/integration/homepage.spec.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { testA11y } from 'cypress/support/utils'; - -describe('Homepage', () => { - beforeEach(() => { - // All tests start with visiting homepage - cy.visit('/'); - }); - - it('should display translated title "DSpace Angular :: Home"', () => { - cy.title().should('eq', 'DSpace Angular :: Home'); - }); - - it('should contain a news section', () => { - cy.get('ds-home-news').should('be.visible'); - }); - - it('should have a working search box', () => { - const queryString = 'test'; - cy.get('[data-test="search-box"]').type(queryString); - cy.get('[data-test="search-button"]').click(); - cy.url().should('include', '/search'); - cy.url().should('include', 'query=' + encodeURI(queryString)); - }); - - it('should pass accessibility tests', () => { - // Wait for homepage tag to appear - cy.get('ds-home-page').should('be.visible'); - - // Analyze for accessibility issues - testA11y('ds-home-page'); - }); -}); diff --git a/cypress/integration/item-page.spec.ts b/cypress/integration/item-page.spec.ts deleted file mode 100644 index 6a454b678d1..00000000000 --- a/cypress/integration/item-page.spec.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Options } from 'cypress-axe'; -import { TEST_ENTITY_PUBLICATION } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -describe('Item Page', () => { - const ITEMPAGE = '/items/' + TEST_ENTITY_PUBLICATION; - const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION; - - // Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid] - it('should redirect to the entity page when navigating to an item page', () => { - cy.visit(ITEMPAGE); - cy.location('pathname').should('eq', ENTITYPAGE); - }); - - it('should pass accessibility tests', () => { - cy.visit(ENTITYPAGE); - - // tag must be loaded - cy.get('ds-item-page').should('exist'); - - // Analyze for accessibility issues - // Disable heading-order checks until it is fixed - testA11y('ds-item-page', - { - rules: { - 'heading-order': { enabled: false } - } - } as Options - ); - }); -}); diff --git a/cypress/integration/item-statistics.spec.ts b/cypress/integration/item-statistics.spec.ts deleted file mode 100644 index 66ebc228dbb..00000000000 --- a/cypress/integration/item-statistics.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { TEST_ENTITY_PUBLICATION } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -describe('Item Statistics Page', () => { - const ITEMSTATISTICSPAGE = '/statistics/items/' + TEST_ENTITY_PUBLICATION; - - it('should load if you click on "Statistics" from an Item/Entity page', () => { - cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION); - cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click(); - cy.location('pathname').should('eq', ITEMSTATISTICSPAGE); - }); - - it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => { - cy.visit(ITEMSTATISTICSPAGE); - cy.get('ds-item-statistics-page').should('exist'); - cy.get('ds-item-page').should('not.exist'); - }); - - it('should contain a "Total visits" section', () => { - cy.visit(ITEMSTATISTICSPAGE); - cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisits').should('exist'); - }); - - it('should contain a "Total visits per month" section', () => { - cy.visit(ITEMSTATISTICSPAGE); - cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisitsPerMonth').should('exist'); - }); - - it('should pass accessibility tests', () => { - cy.visit(ITEMSTATISTICSPAGE); - - // tag must be loaded - cy.get('ds-item-statistics-page').should('exist'); - - // Analyze for accessibility issues - testA11y('ds-item-statistics-page'); - }); -}); diff --git a/cypress/integration/login-modal.spec.ts b/cypress/integration/login-modal.spec.ts deleted file mode 100644 index fece28b4254..00000000000 --- a/cypress/integration/login-modal.spec.ts +++ /dev/null @@ -1,126 +0,0 @@ -import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support'; - -const page = { - openLoginMenu() { - // Click the "Log In" dropdown menu in header - cy.get('ds-themed-navbar [data-test="login-menu"]').click(); - }, - openUserMenu() { - // Once logged in, click the User menu in header - cy.get('ds-themed-navbar [data-test="user-menu"]').click(); - }, - submitLoginAndPasswordByPressingButton(email, password) { - // Enter email - cy.get('ds-themed-navbar [data-test="email"]').type(email); - // Enter password - cy.get('ds-themed-navbar [data-test="password"]').type(password); - // Click login button - cy.get('ds-themed-navbar [data-test="login-button"]').click(); - }, - submitLoginAndPasswordByPressingEnter(email, password) { - // In opened Login modal, fill out email & password, then click Enter - cy.get('ds-themed-navbar [data-test="email"]').type(email); - cy.get('ds-themed-navbar [data-test="password"]').type(password); - cy.get('ds-themed-navbar [data-test="password"]').type('{enter}'); - }, - submitLogoutByPressingButton() { - // This is the POST command that will actually log us out - cy.intercept('POST', '/server/api/authn/logout').as('logout'); - // Click logout button - cy.get('ds-themed-navbar [data-test="logout-button"]').click(); - // Wait until above POST command responds before continuing - // (This ensures next action waits until logout completes) - cy.wait('@logout'); - } -}; - -describe('Login Modal', () => { - it('should login when clicking button & stay on same page', () => { - const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION; - cy.visit(ENTITYPAGE); - - // Login menu should exist - cy.get('ds-log-in').should('exist'); - - // Login, and the tag should no longer exist - page.openLoginMenu(); - cy.get('.form-login').should('be.visible'); - - page.submitLoginAndPasswordByPressingButton(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); - cy.get('ds-log-in').should('not.exist'); - - // Verify we are still on the same page - cy.url().should('include', ENTITYPAGE); - - // Open user menu, verify user menu & logout button now available - page.openUserMenu(); - cy.get('ds-user-menu').should('be.visible'); - cy.get('ds-log-out').should('be.visible'); - }); - - it('should login when clicking enter key & stay on same page', () => { - cy.visit('/home'); - - // Open login menu in header & verify tag is visible - page.openLoginMenu(); - cy.get('.form-login').should('be.visible'); - - // Login, and the tag should no longer exist - page.submitLoginAndPasswordByPressingEnter(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); - cy.get('.form-login').should('not.exist'); - - // Verify we are still on homepage - cy.url().should('include', '/home'); - - // Open user menu, verify user menu & logout button now available - page.openUserMenu(); - cy.get('ds-user-menu').should('be.visible'); - cy.get('ds-log-out').should('be.visible'); - }); - - it('should support logout', () => { - // First authenticate & access homepage - cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD); - cy.visit('/'); - - // Verify ds-log-in tag doesn't exist, but ds-log-out tag does exist - cy.get('ds-log-in').should('not.exist'); - cy.get('ds-log-out').should('exist'); - - // Click logout button - page.openUserMenu(); - page.submitLogoutByPressingButton(); - - // Verify ds-log-in tag now exists - cy.get('ds-log-in').should('exist'); - cy.get('ds-log-out').should('not.exist'); - }); - - it('should allow new user registration', () => { - cy.visit('/'); - - page.openLoginMenu(); - - // Registration link should be visible - cy.get('ds-themed-navbar [data-test="register"]').should('be.visible'); - - // Click registration link & you should go to registration page - cy.get('ds-themed-navbar [data-test="register"]').click(); - cy.location('pathname').should('eq', '/register'); - cy.get('ds-register-email').should('exist'); - }); - - it('should allow forgot password', () => { - cy.visit('/'); - - page.openLoginMenu(); - - // Forgot password link should be visible - cy.get('ds-themed-navbar [data-test="forgot"]').should('be.visible'); - - // Click link & you should go to Forgot Password page - cy.get('ds-themed-navbar [data-test="forgot"]').click(); - cy.location('pathname').should('eq', '/forgot'); - cy.get('ds-forgot-email').should('exist'); - }); -}); diff --git a/cypress/integration/my-dspace.spec.ts b/cypress/integration/my-dspace.spec.ts deleted file mode 100644 index eb931adda73..00000000000 --- a/cypress/integration/my-dspace.spec.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { Options } from 'cypress-axe'; -import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -describe('My DSpace page', () => { - it('should display recent submissions and pass accessibility tests', () => { - cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); - - cy.visit('/mydspace'); - - cy.get('ds-my-dspace-page').should('exist'); - - // At least one recent submission should be displayed - cy.get('[data-test="list-object"]').should('be.visible'); - - // Click each filter toggle to open *every* filter - // (As we want to scan filter section for accessibility issues as well) - cy.get('.filter-toggle').click({ multiple: true }); - - // Analyze for accessibility issues - testA11y( - { - include: ['ds-my-dspace-page'], - exclude: [ - ['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175 - ], - }, - { - rules: { - // Search filters fail these two "moderate" impact rules - 'heading-order': { enabled: false }, - 'landmark-unique': { enabled: false } - } - } as Options - ); - }); - - it('should have a working detailed view that passes accessibility tests', () => { - cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); - - cy.visit('/mydspace'); - - cy.get('ds-my-dspace-page').should('exist'); - - // Click button in sidebar to display detailed view - cy.get('ds-search-sidebar [data-test="detail-view"]').click(); - - cy.get('ds-object-detail').should('exist'); - - // Analyze for accessibility issues - testA11y('ds-my-dspace-page', - { - rules: { - // Search filters fail these two "moderate" impact rules - 'heading-order': { enabled: false }, - 'landmark-unique': { enabled: false } - } - } as Options - ); - }); - - // NOTE: Deleting existing submissions is exercised by submission.spec.ts - it('should let you start a new submission & edit in-progress submissions', () => { - cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); - cy.visit('/mydspace'); - - // Open the New Submission dropdown - cy.get('#dropdownSubmission').click(); - // Click on the "Item" type in that dropdown - cy.get('#entityControlsDropdownMenu button[title="none"]').click(); - - // This should display the (popup window) - cy.get('ds-create-item-parent-selector').should('be.visible'); - - // Type in a known Collection name in the search box - cy.get('ds-authorized-collection-selector input[type="search"]').type(TEST_SUBMIT_COLLECTION_NAME); - - // Click on the button matching that known Collection name - cy.get('ds-authorized-collection-selector button[title="' + TEST_SUBMIT_COLLECTION_NAME + '"]').click(); - - // New URL should include /workspaceitems, as we've started a new submission - cy.url().should('include', '/workspaceitems'); - - // The Submission edit form tag should be visible - cy.get('ds-submission-edit').should('be.visible'); - - // A Collection menu button should exist & its value should be the selected collection - cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME); - - // Now that we've created a submission, we'll test that we can go back and Edit it. - // Get our Submission URL, to parse out the ID of this new submission - cy.location().then(fullUrl => { - // This will be the full path (/workspaceitems/[id]/edit) - const path = fullUrl.pathname; - // Split on the slashes - const subpaths = path.split('/'); - // Part 2 will be the [id] of the submission - const id = subpaths[2]; - - // Click the "Save for Later" button to save this submission - cy.get('button#saveForLater').click(); - - // "Save for Later" should send us to MyDSpace - cy.url().should('include', '/mydspace'); - - // Close any open notifications, to make sure they don't get in the way of next steps - cy.get('[data-dismiss="alert"]').click({multiple: true}); - - // This is the GET command that will actually run the search - cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); - // On MyDSpace, find the submission we just created via its ID - cy.get('[data-test="search-box"]').type(id); - cy.get('[data-test="search-button"]').click(); - - // Wait for search results to come back from the above GET command - cy.wait('@search-results'); - - // Click the Edit button for this in-progress submission - cy.get('#edit_' + id).click(); - - // Should send us back to the submission form - cy.url().should('include', '/workspaceitems/' + id + '/edit'); - - // Discard our new submission by clicking Discard in Submission form & confirming - cy.get('button#discard').click(); - cy.get('button#discard_submit').click(); - - // Discarding should send us back to MyDSpace - cy.url().should('include', '/mydspace'); - }); - }); - - it('should let you import from external sources', () => { - cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); - cy.visit('/mydspace'); - - // Open the New Import dropdown - cy.get('#dropdownImport').click(); - // Click on the "Item" type in that dropdown - cy.get('#importControlsDropdownMenu button[title="none"]').click(); - - // New URL should include /import-external, as we've moved to the import page - cy.url().should('include', '/import-external'); - - // The external import searchbox should be visible - cy.get('ds-submission-import-external-searchbar').should('be.visible'); - }); - -}); diff --git a/cypress/integration/pagenotfound.spec.ts b/cypress/integration/pagenotfound.spec.ts deleted file mode 100644 index 48520bcaa32..00000000000 --- a/cypress/integration/pagenotfound.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -describe('PageNotFound', () => { - it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => { - // request an invalid page (UUIDs at root path aren't valid) - cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false }); - cy.get('ds-pagenotfound').should('exist'); - }); - - it('should not contain element ds-pagenotfound when navigating to existing page', () => { - cy.visit('/home'); - cy.get('ds-pagenotfound').should('not.exist'); - }); - -}); diff --git a/cypress/integration/search-navbar.spec.ts b/cypress/integration/search-navbar.spec.ts deleted file mode 100644 index babd9b9dfdf..00000000000 --- a/cypress/integration/search-navbar.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { TEST_SEARCH_TERM } from 'cypress/support'; - -const page = { - fillOutQueryInNavBar(query) { - // Click the magnifying glass - cy.get('ds-themed-navbar [data-test="header-search-icon"]').click(); - // Fill out a query in input that appears - cy.get('ds-themed-navbar [data-test="header-search-box"]').type(query); - }, - submitQueryByPressingEnter() { - cy.get('ds-themed-navbar [data-test="header-search-box"]').type('{enter}'); - }, - submitQueryByPressingIcon() { - cy.get('ds-themed-navbar [data-test="header-search-icon"]').click(); - } -}; - -describe('Search from Navigation Bar', () => { - // NOTE: these tests currently assume this query will return results! - const query = TEST_SEARCH_TERM; - - it('should go to search page with correct query if submitted (from home)', () => { - cy.visit('/'); - // This is the GET command that will actually run the search - cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); - // Run the search - page.fillOutQueryInNavBar(query); - page.submitQueryByPressingEnter(); - // New URL should include query param - cy.url().should('include', 'query=' + query); - // Wait for search results to come back from the above GET command - cy.wait('@search-results'); - // At least one search result should be displayed - cy.get('[data-test="list-object"]').should('be.visible'); - }); - - it('should go to search page with correct query if submitted (from search)', () => { - cy.visit('/search'); - // This is the GET command that will actually run the search - cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); - // Run the search - page.fillOutQueryInNavBar(query); - page.submitQueryByPressingEnter(); - // New URL should include query param - cy.url().should('include', 'query=' + query); - // Wait for search results to come back from the above GET command - cy.wait('@search-results'); - // At least one search result should be displayed - cy.get('[data-test="list-object"]').should('be.visible'); - }); - - it('should allow user to also submit query by clicking icon', () => { - cy.visit('/'); - // This is the GET command that will actually run the search - cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); - // Run the search - page.fillOutQueryInNavBar(query); - page.submitQueryByPressingIcon(); - // New URL should include query param - cy.url().should('include', 'query=' + query); - // Wait for search results to come back from the above GET command - cy.wait('@search-results'); - // At least one search result should be displayed - cy.get('[data-test="list-object"]').should('be.visible'); - }); -}); diff --git a/cypress/integration/search-page.spec.ts b/cypress/integration/search-page.spec.ts deleted file mode 100644 index de279c7f2e1..00000000000 --- a/cypress/integration/search-page.spec.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Options } from 'cypress-axe'; -import { TEST_SEARCH_TERM } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -describe('Search Page', () => { - it('should redirect to the correct url when query was set and submit button was triggered', () => { - const queryString = 'Another interesting query string'; - cy.visit('/search'); - // Type query in searchbox & click search button - cy.get('[data-test="search-box"]').type(queryString); - cy.get('[data-test="search-button"]').click(); - cy.url().should('include', 'query=' + encodeURI(queryString)); - }); - - it('should load results and pass accessibility tests', () => { - cy.visit('/search?query=' + TEST_SEARCH_TERM); - cy.get('[data-test="search-box"]').should('have.value', TEST_SEARCH_TERM); - - // tag must be loaded - cy.get('ds-search-page').should('exist'); - - // At least one search result should be displayed - cy.get('[data-test="list-object"]').should('be.visible'); - - // Click each filter toggle to open *every* filter - // (As we want to scan filter section for accessibility issues as well) - cy.get('.filter-toggle').click({ multiple: true }); - - // Analyze for accessibility issues - testA11y( - { - include: ['ds-search-page'], - exclude: [ - ['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175 - ], - }, - { - rules: { - // Search filters fail these two "moderate" impact rules - 'heading-order': { enabled: false }, - 'landmark-unique': { enabled: false } - } - } as Options - ); - }); - - it('should have a working grid view that passes accessibility tests', () => { - cy.visit('/search?query=' + TEST_SEARCH_TERM); - - // Click button in sidebar to display grid view - cy.get('ds-search-sidebar [data-test="grid-view"]').click(); - - // tag must be loaded - cy.get('ds-search-page').should('exist'); - - // At least one grid object (card) should be displayed - cy.get('[data-test="grid-object"]').should('be.visible'); - - // Analyze for accessibility issues - testA11y('ds-search-page', - { - rules: { - // Search filters fail these two "moderate" impact rules - 'heading-order': { enabled: false }, - 'landmark-unique': { enabled: false } - } - } as Options - ); - }); -}); diff --git a/cypress/integration/submission.spec.ts b/cypress/integration/submission.spec.ts deleted file mode 100644 index c877479f442..00000000000 --- a/cypress/integration/submission.spec.ts +++ /dev/null @@ -1,134 +0,0 @@ -import { Options } from 'cypress-axe'; -import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support'; -import { testA11y } from 'cypress/support/utils'; - -describe('New Submission page', () => { - // NOTE: We already test that new submissions can be started from MyDSpace in my-dspace.spec.ts - - it('should create a new submission when using /submit path & pass accessibility', () => { - cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); - - // Test that calling /submit with collection & entityType will create a new submission - cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); - - // Should redirect to /workspaceitems, as we've started a new submission - cy.url().should('include', '/workspaceitems'); - - // The Submission edit form tag should be visible - cy.get('ds-submission-edit').should('be.visible'); - - // A Collection menu button should exist & it's value should be the selected collection - cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME); - - // 4 sections should be visible by default - cy.get('div#section_traditionalpageone').should('be.visible'); - cy.get('div#section_traditionalpagetwo').should('be.visible'); - cy.get('div#section_upload').should('be.visible'); - cy.get('div#section_license').should('be.visible'); - - // Discard button should work - // Clicking it will display a confirmation, which we will confirm with another click - cy.get('button#discard').click(); - cy.get('button#discard_submit').click(); - }); - - it('should block submission & show errors if required fields are missing', () => { - cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); - - // Create a new submission - cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); - - // Attempt an immediate deposit without filling out any fields - cy.get('button#deposit').click(); - - // A warning alert should display. - cy.get('ds-notification div.alert-warning').should('be.visible'); - - // First section should have an exclamation error in the header - // (as it has required fields) - cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible'); - - // Title field should have class "is-invalid" applied, as it's required - cy.get('input#dc_title').should('have.class', 'is-invalid'); - - // Date Year field should also have "is-valid" class - cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid'); - - // FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button. - // Get our Submission URL, to parse out the ID of this submission - cy.location().then(fullUrl => { - // This will be the full path (/workspaceitems/[id]/edit) - const path = fullUrl.pathname; - // Split on the slashes - const subpaths = path.split('/'); - // Part 2 will be the [id] of the submission - const id = subpaths[2]; - - // Even though form is incomplete, the "Save for Later" button should still work - cy.get('button#saveForLater').click(); - - // "Save for Later" should send us to MyDSpace - cy.url().should('include', '/mydspace'); - - // A success alert should be visible - cy.get('ds-notification div.alert-success').should('be.visible'); - // Now, dismiss any open alert boxes (may be multiple, as tests run quickly) - cy.get('[data-dismiss="alert"]').click({multiple: true}); - - // This is the GET command that will actually run the search - cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results'); - // On MyDSpace, find the submission we just saved via its ID - cy.get('[data-test="search-box"]').type(id); - cy.get('[data-test="search-button"]').click(); - - // Wait for search results to come back from the above GET command - cy.wait('@search-results'); - - // Delete our created submission & confirm deletion - cy.get('button#delete_' + id).click(); - cy.get('button#delete_confirm').click(); - }); - }); - - it('should allow for deposit if all required fields completed & file uploaded', () => { - cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD); - - // Create a new submission - cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none'); - - // Fill out all required fields (Title, Date) - cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests'); - cy.get('input#dc_date_issued_year').type('2022'); - - // Confirm the required license by checking checkbox - // (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own ) - cy.get('input#granted').check( {force: true} ); - - // Before using Cypress drag & drop, we have to manually trigger the "dragover" event. - // This ensures our UI displays the dropzone that covers the entire submission page. - // (For some reason Cypress drag & drop doesn't trigger this even itself & upload won't work without this trigger) - cy.get('ds-uploader').trigger('dragover'); - - // This is the POST command that will upload the file - cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload'); - - // Upload our DSpace logo via drag & drop onto submission form - // cy.get('div#section_upload') - cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.png', { - action: 'drag-drop' - }); - - // Wait for upload to complete before proceeding - cy.wait('@upload'); - // Close the upload success notice - cy.get('[data-dismiss="alert"]').click({multiple: true}); - - // Wait for deposit button to not be disabled & click it. - cy.get('button#deposit').should('not.be.disabled').click(); - - // No warnings should exist. Instead, just successful deposit alert is displayed - cy.get('ds-notification div.alert-warning').should('not.exist'); - cy.get('ds-notification div.alert-success').should('be.visible'); - }); - -}); diff --git a/cypress/plugins/index.ts b/cypress/plugins/index.ts index ead38afb921..cc3dccba38e 100644 --- a/cypress/plugins/index.ts +++ b/cypress/plugins/index.ts @@ -1,5 +1,11 @@ const fs = require('fs'); +// These two global variables are used to store information about the REST API used +// by these e2e tests. They are filled out prior to running any tests in the before() +// method of e2e.ts. They can then be accessed by any tests via the getters below. +let REST_BASE_URL: string; +let REST_DOMAIN: string; + // Plugins enable you to tap into, modify, or extend the internal behavior of Cypress // For more info, visit https://on.cypress.io/plugins-api module.exports = (on, config) => { @@ -30,6 +36,24 @@ module.exports = (on, config) => { } return null; + }, + // Save value of REST Base URL, looked up before all tests. + // This allows other tests to use it easily via getRestBaseURL() below. + saveRestBaseURL(url: string) { + return (REST_BASE_URL = url); + }, + // Retrieve currently saved value of REST Base URL + getRestBaseURL() { + return REST_BASE_URL ; + }, + // Save value of REST Domain, looked up before all tests. + // This allows other tests to use it easily via getRestBaseDomain() below. + saveRestBaseDomain(domain: string) { + return (REST_DOMAIN = domain); + }, + // Retrieve currently saved value of REST Domain + getRestBaseDomain() { + return REST_DOMAIN ; } }); }; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 30951d46f1e..3db9f351fec 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -4,12 +4,13 @@ // *********************************************** import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model'; -import { FALLBACK_TEST_REST_BASE_URL } from '.'; +import { DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER } from 'src/app/core/xsrf/xsrf.constants'; +import { v4 as uuidv4 } from 'uuid'; // Declare Cypress namespace to help with Intellisense & code completion in IDEs // ALL custom commands MUST be listed here for code completion to work -// tslint:disable-next-line:no-namespace declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace namespace Cypress { interface Chainable { /** @@ -19,6 +20,30 @@ declare global { * @param password password to login as */ login(email: string, password: string): typeof login; + + /** + * Login via form before accessing the next page. Useful to fill out login + * form when a cy.visit() call is to an a page which requires authentication. + * @param email email to login as + * @param password password to login as + */ + loginViaForm(email: string, password: string): typeof loginViaForm; + + /** + * Generate view event for given object. Useful for testing statistics pages with + * pre-generated statistics. This just generates a single "hit", but can be called multiple times to + * generate multiple hits. + * @param uuid UUID of object + * @param dsoType type of DSpace Object (e.g. "item", "collection", "community") + */ + generateViewEvent(uuid: string, dsoType: string): typeof generateViewEvent; + + /** + * Create a new CSRF token and add to required Cookie. CSRF Token is returned + * in chainable in order to allow it to be sent also in required CSRF header. + * @returns Chainable reference to allow CSRF token to also be sent in header. + */ + createCSRFCookie(): Chainable; } } } @@ -26,39 +51,21 @@ declare global { /** * Login user via REST API directly, and pass authentication token to UI via * the UI's dsAuthInfo cookie. + * WARNING: WHILE THIS METHOD WORKS, OCCASIONALLY RANDOM AUTHENTICATION ERRORS OCCUR. + * At this time "loginViaForm()" seems more consistent/stable. * @param email email to login as * @param password password to login as */ function login(email: string, password: string): void { - // Cypress doesn't have access to the running application in Node.js. - // So, it's not possible to inject or load the AppConfig or environment of the Angular UI. - // Instead, we'll read our running application's config.json, which contains the configs & - // is regenerated at runtime each time the Angular UI application starts up. - cy.task('readUIConfig').then((str: string) => { - // Parse config into a JSON object - const config = JSON.parse(str); - - // Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found. - let baseRestUrl = FALLBACK_TEST_REST_BASE_URL; - if (!config.rest.baseUrl) { - console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL); - } else { - console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: " + config.rest.baseUrl); - baseRestUrl = config.rest.baseUrl; - } - - // To login via REST, first we have to do a GET to obtain a valid CSRF token - cy.request( baseRestUrl + '/api/authn/status' ) - .then((response) => { - // We should receive a CSRF token returned in a response header - expect(response.headers).to.have.property('dspace-xsrf-token'); - const csrfToken = response.headers['dspace-xsrf-token']; - + // Create a fake CSRF cookie/token to use in POST + cy.createCSRFCookie().then((csrfToken: string) => { + // get our REST API's base URL, also needed for POST + cy.task('getRestBaseURL').then((baseRestUrl: string) => { // Now, send login POST request including that CSRF token cy.request({ method: 'POST', url: baseRestUrl + '/api/authn/login', - headers: { 'X-XSRF-TOKEN' : csrfToken}, + headers: { [XSRF_REQUEST_HEADER]: csrfToken}, form: true, // indicates the body should be form urlencoded body: { user: email, password: password } }).then((resp) => { @@ -76,8 +83,279 @@ function login(email: string, password: string): void { cy.setCookie(TOKENITEM, JSON.stringify(authinfo)); }); }); - }); } // Add as a Cypress command (i.e. assign to 'cy.login') Cypress.Commands.add('login', login); + +/** + * Login user via displayed login form + * @param email email to login as + * @param password password to login as + */ +// Cypress custom command for form-based login with intercept and redirect assertion +function loginViaForm( + email: string, + password: string +): void { + // Optionally close the DiscoJuice popup if present + cy.wait(500); + cy.get('.discojuice_close').should('exist').click(); + + // Fill in credentials + cy.get('[data-test="email"]').should('be.visible').type(email); + cy.get('[data-test="password"]').type(password); + + // Submit the form + cy.get('[data-test="login-button"]').click(); + +} +// Add as a Cypress command (i.e. assign to 'cy.loginViaForm') +Cypress.Commands.add('loginViaForm', loginViaForm); + +// Do not fail test if an uncaught exception occurs in the application +Cypress.on('uncaught:exception', (err, runnable) => { + // returning false here prevents Cypress from + // failing the test + return false; +}); + + +/** + * Generate statistic view event for given object. Useful for testing statistics pages with + * pre-generated statistics. This just generates a single "hit", but can be called multiple times to + * generate multiple hits. + * + * NOTE: This requires that "solr-statistics.autoCommit=false" be set on the DSpace backend + * (as it is in our docker-compose-ci.yml used in CI). + * Otherwise, by default, new statistical events won't be saved until Solr's autocommit next triggers. + * @param uuid UUID of object + * @param dsoType type of DSpace Object (e.g. "item", "collection", "community") + */ +function generateViewEvent(uuid: string, dsoType: string): void { + // Create a fake CSRF cookie/token to use in POST + cy.createCSRFCookie().then((csrfToken: string) => { + // get our REST API's base URL, also needed for POST + cy.task('getRestBaseURL').then((baseRestUrl: string) => { + // Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header + cy.request({ + method: 'POST', + url: baseRestUrl + '/api/statistics/viewevents', + headers: { + [XSRF_REQUEST_HEADER] : csrfToken, + // use a known public IP address to avoid being seen as a "bot" + 'X-Forwarded-For': '1.1.1.1', + // Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot" + 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0', + }, + //form: true, // indicates the body should be form urlencoded + body: { targetId: uuid, targetType: dsoType }, + }).then((resp) => { + // We expect a 201 (which means statistics event was created) + expect(resp.status).to.eq(201); + }); + }); + }); +} +// Add as a Cypress command (i.e. assign to 'cy.generateViewEvent') +Cypress.Commands.add('generateViewEvent', generateViewEvent); + +export const loginProcess = { + clickOnLoginDropdown() { + cy.get('.navbar-container .dropdownLogin ').click(); + }, + typeEmail(email: string) { + cy.get('ds-log-in-container form input[type = "email"] ').type(email); + }, + typePassword(password: string) { + cy.get('ds-log-in-container form input[type = "password"] ').type(password); + }, + submit() { + cy.get('ds-log-in-container form button[type = "submit"] ').click(); + }, + login(email: string, password: string) { + cy.visit('/login'); + // loginProcess.clickOnLoginDropdown(); + loginProcess.typeEmail(email); + loginProcess.typePassword(password); + loginProcess.submit(); + // wait for redirecting after login - end of login process + cy.url().should('contain', '/home'); + } +}; + +export const createItemProcess = { + checkLocalHasCMDIVisibility() { + cy.get('#traditionalpageone form div[role = "group"] label[for = "local_hasCMDI"]').should('be.visible'); + }, + checkIsInputVisible(inputName, formatted = false, inputOrder = 0) { + let inputNameTag = 'input['; + inputNameTag += formatted ? 'ng-reflect-name' : 'name'; + inputNameTag += ' = '; + + cy.get('#traditionalpageone form div[role = "group"] ' + inputNameTag + '"' + inputName + '"]') + .eq(inputOrder).should('be.visible'); + }, + checkIsNotInputVisible(inputName, formatted = false, inputOrder = 0) { + let inputNameTag = 'input['; + inputNameTag += formatted ? 'ng-reflect-name' : 'name'; + inputNameTag += ' = '; + + cy.get('#traditionalpageone form div[role = "group"] ' + inputNameTag + '"' + inputName + '"]') + .eq(inputOrder).should('not.be.visible'); + }, + clickOnSelectionInput(inputName, inputOrder = 0) { + cy.get('#traditionalpageone form div[role = "group"] input[name = "' + inputName + '"]').eq(inputOrder).click(); + }, + clickOnInput(inputName, force = false) { + cy.get('#traditionalpageone form div[role = "group"] input[ng-reflect-name = "' + inputName + '"]') + .click(force ? {force: true} : {}); + }, + writeValueToInput(inputName, value, formatted = false, inputOrder = 0) { + if (formatted) { + cy.get('#traditionalpageone form div[role = "group"] input[ng-reflect-name = "' + inputName + '"]').eq(inputOrder).click({force: true}).type(value); + } else { + cy.get('#traditionalpageone form div[role = "group"] input[name = "' + inputName + '"]').eq(inputOrder).click({force: true}).type(value); + } + }, + blurInput(inputName, formatted) { + if (formatted) { + cy.get('#traditionalpageone form div[role = "group"] input[ng-reflect-name = "' + inputName + '"]').blur(); + } else { + cy.get('#traditionalpageone form div[role = "group"] input[name = "' + inputName + '"]').blur(); + } + }, + clickOnTypeSelection(selectionName) { + cy.get('#traditionalpageone form div[role = "group"] div[role = "listbox"]' + + ' button[title = "' + selectionName + '"]').click(); + }, + clickOnSuggestionSelection(selectionNumber) { + cy.get('#traditionalpageone form div[role = "group"] ngb-typeahead-window[role = "listbox"]' + + ' button[type = "button"]').eq(selectionNumber).click(); + }, + + clickOnDivById(id, force) { + cy.get('div[id = "' + id + '"]').click(force ? {force: true} : {}); + }, + checkInputValue(inputName, observedInputValue) { + cy.get('#traditionalpageone form div[role = "group"] div[role = "combobox"] input[name = "' + inputName + '"]') + .should('contain',observedInputValue); + }, + checkCheckbox(inputName) { + cy.get('#traditionalpageone form div[role = "group"] div[id = "' + inputName + '"] input[type = "checkbox"]') + .check({force: true}); + }, + controlCheckedCheckbox(inputName, checked) { + const checkedCondition = checked === true ? 'be.checked' : 'not.be.checked'; + cy.get('#traditionalpageone form div[role = "group"] div[id = "' + inputName + '"] input[type = "checkbox"]') + .should(checkedCondition); + }, + clickOnSave() { + cy.get('.submission-form-footer button[id = "save"]').click(); + }, + clickOnSelection(nameOfSelection, optionNumber) { + cy.get('.dropdown-menu button[title="' + nameOfSelection + '"]').eq(optionNumber).click(); + }, + clickAddMore(inputFieldOrder) { + cy.get('#traditionalpageone form div[role = "group"] button[title = "Add more"]').eq(inputFieldOrder) + .click({force: true}); + }, + checkDistributionLicenseStep() { + cy.get('ds-clarin-license-distribution').should('be.visible'); + }, + checkDistributionLicenseToggle() { + cy.get('ds-clarin-license-distribution ng-toggle').should('be.visible'); + }, + checkDistributionLicenseStatus(statusTitle: string) { + cy.get('div[id = "license-header"] button i[title = "' + statusTitle + '"]').should('be.visible'); + }, + clickOnDistributionLicenseToggle() { + cy.get('ds-clarin-license-distribution ng-toggle').click(); + }, + checkLicenseResourceStep() { + cy.get('ds-submission-section-clarin-license').should('be.visible'); + }, + checkClarinNoticeStep() { + cy.get('ds-clarin-notice').should('be.visible'); + }, + checkClarinNoticeStepNotExist() { + cy.get('ds-clarin-notice').should('not.exist'); + }, + clickOnLicenseSelectorButton() { + cy.get('ds-submission-section-clarin-license div[id = "aspect_submission_StepTransformer_item_"] button').click(); + }, + checkLicenseSelectorModal() { + cy.get('section[class = "license-selector is-active"]').should('be.visible'); + }, + pickUpLicenseFromLicenseSelector() { + cy.get('section[class = "license-selector is-active"] ul li').eq(0).dblclick(); + }, + checkLicenseSelectionValue(value: string) { + cy.get('ds-submission-section-clarin-license input[id = "aspect_submission_StepTransformer_field_license"]').should('have.value', value); + }, + selectValueFromLicenseSelection(id: number) { + cy.get('ds-submission-section-clarin-license li[value = "' + id + '"]').click(); + }, + clickOnLicenseSelectionButton() { + cy.get('ds-submission-section-clarin-license input[id = "aspect_submission_StepTransformer_field_license"]').click(); + }, + checkResourceLicenseStatus(statusTitle: string) { + // cy.get('div[id = "clarin-license-header"] button i[title = "' + statusTitle + '"]').should('be.visible'); + const iconMap = { + 'Warnings': '.fa-exclamation-circle.text-warning', + 'Errors': '.fa-exclamation-circle.text-danger', + 'Valid': '.fa-check-circle.text-success' + }; + const iconSelector = iconMap[statusTitle]; + cy.get('div[id="section_clarin-license"]') + .find('.card-header') + .find(iconSelector) + .should('be.visible'); + }, + showErrorMustChooseLicense() { + cy.get('div[id = "sectionGenericError_clarin-license"] ds-alert').contains('You must choose one of the resource licenses.'); + }, + showErrorNotSupportedLicense() { + cy.get('div[class = "form-group alert alert-danger in"]').contains('The selected license is not supported at the moment. Please follow the procedure described under section "None of these licenses suits your needs".'); + }, + checkAuthorLastnameField() { + cy.get('ds-dynamic-autocomplete input[placeholder = "Last name"]').should('be.visible'); + }, + checkAuthorLastnameFieldValue(value) { + cy.get('ds-dynamic-autocomplete input[placeholder = "Last name"]').should('have.value', value); + }, + checkAuthorFirstnameField() { + cy.get('dynamic-ng-bootstrap-input input[placeholder = "First name"]').should('be.visible'); + }, + checkAuthorFirstnameFieldValue(value) { + cy.get('dynamic-ng-bootstrap-input input[placeholder = "First name"]').should('have.value', value); + }, + writeAuthorInputField(value) { + cy.get('ds-dynamic-autocomplete input[placeholder = "Last name"]').eq(0).click({force: true}).type(value); + } +}; + + + +/** + * Can be used by tests to generate a random XSRF/CSRF token and save it to + * the required XSRF/CSRF cookie for usage when sending POST requests or similar. + * The generated CSRF token is returned in a Chainable to allow it to be also sent + * in the CSRF HTTP Header. + * @returns a Cypress Chainable which can be used to get the generated CSRF Token + */ +function createCSRFCookie(): Cypress.Chainable { + // Generate a new token which is a random UUID + const csrfToken: string = uuidv4(); + + // Save it to our required cookie + cy.task('getRestBaseDomain').then((baseDomain: string) => { + // Create a fake CSRF Token. Set it in the required server-side cookie + cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain }); + }); + + // return the generated token wrapped in a chainable + return cy.wrap(csrfToken); +} +// Add as a Cypress command (i.e. assign to 'cy.createCSRFCookie') +Cypress.Commands.add('createCSRFCookie', createCSRFCookie); diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 00000000000..10e785e1845 --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,104 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import all custom Commands (from commands.ts) for all tests +import './commands'; + +// Import Cypress Axe tools for all tests +// https://github.com/component-driven/cypress-axe +import 'cypress-axe'; +import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants'; + + +// Runs once before all tests +before(() => { + // Cypress doesn't have access to the running application in Node.js. + // So, it's not possible to inject or load the AppConfig or environment of the Angular UI. + // Instead, we'll read our running application's config.json, which contains the configs & + // is regenerated at runtime each time the Angular UI application starts up. + cy.task('readUIConfig').then((str: string) => { + // Parse config into a JSON object + const config = JSON.parse(str); + + // Find URL of our REST API & save to global variable via task + let baseRestUrl = FALLBACK_TEST_REST_BASE_URL; + if (!config.rest.baseUrl) { + console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL); + } else { + baseRestUrl = config.rest.baseUrl; + } + cy.task('saveRestBaseURL', baseRestUrl); + + // Find domain of our REST API & save to global variable via task. + let baseDomain = FALLBACK_TEST_REST_DOMAIN; + if (!config.rest.host) { + console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN); + } else { + baseDomain = config.rest.host; + } + cy.task('saveRestBaseDomain', baseDomain); + + }); +}); + +// Runs once before the first test in each "block" +beforeEach(() => { + // Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie + // This just ensures it doesn't get in the way of matching other objects in the page. + cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}'); + + // Remove any CSRF cookies saved from prior tests + cy.clearCookie(DSPACE_XSRF_COOKIE); +}); + +// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL +// from the Angular UI's config.json. See 'before()' above. +const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server'; +const FALLBACK_TEST_REST_DOMAIN = 'localhost'; +// Admin account used for administrative tests +export const TEST_ADMIN_USER = Cypress.env('DSPACE_TEST_ADMIN_USER') || 'dspacedemo+admin@gmail.com'; +export const TEST_ADMIN_PASSWORD = Cypress.env('DSPACE_TEST_ADMIN_PASSWORD') || 'dspace'; +// Community/collection/publication used for view/edit tests +export const TEST_COLLECTION = Cypress.env('DSPACE_TEST_COLLECTION') || '282164f5-d325-4740-8dd1-fa4d6d3e7200'; +export const TEST_COMMUNITY = Cypress.env('DSPACE_TEST_COMMUNITY') || '0958c910-2037-42a9-81c7-dca80e3892b4'; +export const TEST_ENTITY_PUBLICATION = Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION') || 'e98b0f27-5c19-49a0-960d-eb6ad5287067'; +// Search term (should return results) used in search tests +export const TEST_SEARCH_TERM = Cypress.env('DSPACE_TEST_SEARCH_TERM') || 'test'; +// Collection used for submission tests +export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME') || 'Sample Collection'; +export const TEST_SUBMIT_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID') || '9d8334e9-25d3-4a67-9cea-3dffdef80144'; +export const TEST_SUBMIT_USER = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com'; +export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace'; + +export const TEST_SUBMIT_CLARIAH_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_CLARIAH_COLLECTION_UUID') || '7eb3562b-27f5-445f-8303-db771969cbff'; +export const TEST_WITHDRAWN_ITEM = Cypress.env('CLARIN_TEST_WITHDRAWN_ITEM') || '921d256f-c64f-438e-b17e-13fb75a64e19'; +export const TEST_WITHDRAWN_ITEM_WITH_REASON = Cypress.env('CLARIN_TEST_WITHDRAWN_ITEM_WITH_REASON') || 'ce6ceeb4-8f47-4d5a-ad22-e87b3110cc04'; +export const TEST_WITHDRAWN_ITEM_WITH_REASON_AND_AUTHORS = Cypress.env('CLARIN_TEST_WITHDRAWN_ITEM_WITH_REASON_AND_AUTHORS') || 'ad27520a-98c0-40a4-bfc3-2edd857b3418'; +export const TEST_WITHDRAWN_REPLACED_ITEM = Cypress.env('CLARIN_TEST_WITHDRAWN_REPLACED_ITEM') || '94c48fc7-0425-48dc-9be6-7e7087534a3d'; +export const TEST_WITHDRAWN_REPLACED_ITEM_WITH_AUTHORS = Cypress.env('CLARIN_TEST_WITHDRAWN_REPLACED_ITEM_WITH_AUTHORS') || '0e9ef1cb-5b9f-4acc-a7ca-5a9a66a6ddbd'; + +export const TEST_WITHDRAWN_REASON = Cypress.env('CLARIN_TEST_WITHDRAWN_REASON') || 'reason'; +export const TEST_WITHDRAWN_REPLACEMENT = Cypress.env('CLARIN_TEST_WITHDRAWN_REPLACEMENT') || 'new URL'; +export const TEST_WITHDRAWN_AUTHORS = Cypress.env('CLARIN_TEST_WITHDRAWN_AUTHORS') || 'author1, author2'; + +export const TEST_COLLECTION_NAME = 'Col'; +export const TEST_COMMUNITY_NAME = 'Com'; + + +// USEFUL REGEX for testing + +// Match any string that contains at least one non-space character +// Can be used with "contains()" to determine if an element has a non-empty text value +export const REGEX_MATCH_NON_EMPTY_TEXT = /^(?!\s*$).+/; diff --git a/cypress/support/index.ts b/cypress/support/index.ts deleted file mode 100644 index d9b6409a0d9..00000000000 --- a/cypress/support/index.ts +++ /dev/null @@ -1,63 +0,0 @@ -// *********************************************************** -// This example support/index.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - -// Import all custom Commands (from commands.ts) for all tests -import './commands'; - -// Import Cypress Axe tools for all tests -// https://github.com/component-driven/cypress-axe -import 'cypress-axe'; - -// Runs once before the first test in each "block" -before(() => { - // Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie - // This just ensures it doesn't get in the way of matching other objects in the page. - cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true}'); -}); - -// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test. -// This ensures any remaining/outstanding XHR requests are killed, so they don't affect the next test. -// Borrowed from: https://glebbahmutov.com/blog/visit-blank-page-between-tests/ -afterEach(() => { - cy.window().then((win) => { - win.location.href = 'about:blank'; - }); -}); - - -// Global constants used in tests -// May be overridden in our cypress.json config file using specified environment variables. -// Default values listed here are all valid for the Demo Entities Data set available at -// https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data -// (This is the data set used in our CI environment) - -// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL -// from the Angular UI's config.json. See 'getBaseRESTUrl()' in commands.ts -export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server'; - -// Admin account used for administrative tests -export const TEST_ADMIN_USER = Cypress.env('DSPACE_TEST_ADMIN_USER') || 'dspacedemo+admin@gmail.com'; -export const TEST_ADMIN_PASSWORD = Cypress.env('DSPACE_TEST_ADMIN_PASSWORD') || 'dspace'; -// Community/collection/publication used for view/edit tests -export const TEST_COLLECTION = Cypress.env('DSPACE_TEST_COLLECTION') || '282164f5-d325-4740-8dd1-fa4d6d3e7200'; -export const TEST_COMMUNITY = Cypress.env('DSPACE_TEST_COMMUNITY') || '0958c910-2037-42a9-81c7-dca80e3892b4'; -export const TEST_ENTITY_PUBLICATION = Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION') || 'e98b0f27-5c19-49a0-960d-eb6ad5287067'; -// Search term (should return results) used in search tests -export const TEST_SEARCH_TERM = Cypress.env('DSPACE_TEST_SEARCH_TERM') || 'test'; -// Collection used for submission tests -export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME') || 'Sample Collection'; -export const TEST_SUBMIT_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID') || '9d8334e9-25d3-4a67-9cea-3dffdef80144'; -export const TEST_SUBMIT_USER = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com'; -export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace'; diff --git a/docker/README.md b/docker/README.md index d6fe0e6646a..994cc98736e 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,10 +1,25 @@ # Docker Compose files *** -:warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario. +:warning: **THESE IMAGES ARE NOT PRODUCTION READY** The below Docker Compose images/resources were built for development/testing only. Therefore, they may not be fully secured or up-to-date, and should not be used in production. + +If you wish to run DSpace on Docker in production, we recommend building your own Docker images. You are welcome to borrow ideas/concepts from the below images in doing so. But, the below images should not be used "as is" in any production scenario. *** -## 'Dockerfile' in root directory +## Overview +The scripts in this directory can be used to start the DSpace User Interface (frontend) in Docker. +Optionally, the backend (REST API) might also be started in Docker. + +For additional options/settings in starting the backend (REST API) in Docker, see the Docker Compose +documentation for the backend: https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md + +## Root directory + +The root directory of this project contains all the Dockerfiles which may be referenced by +the Docker compose scripts in this 'docker' folder. + +### Dockerfile + This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular' ``` @@ -18,7 +33,18 @@ Admins to our DockerHub repo can manually publish with the following command. docker push dspace/dspace-angular:dspace-7_x ``` -## docker directory +### Dockerfile.dist + +The `Dockerfile.dist` is used to generate a *production* build and runtime environment. + +```bash +# build the latest image +docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist . +``` + +A default/demo version of this image is built *automatically*. + +## 'docker' directory - docker-compose.yml - Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker. - docker-compose-rest.yml @@ -43,28 +69,52 @@ docker-compose -f docker/docker-compose.yml build ## To start DSpace (REST and Angular) from your branch +This command provides a quick way to start both the frontend & backend from this single codebase ``` docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d ``` +Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section. + + ## Run DSpace REST and DSpace Angular from local branches. + +This section assumes that you have clones *both* the 'DSpace/DSpace' and 'DSpace/dspace-angular' GitHub +repositories. When both are available locally, you can spin up both in Docker and have them work together. + _The system will be started in 2 steps. Each step shares the same docker network._ -From DSpace/DSpace (build as needed) +From 'DSpace/DSpace' clone (build first as needed): ``` docker-compose -p d7 up -d ``` -From DSpace/DSpace-angular +NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md). + +From 'DSpace/dspace-angular' clone (build first as needed) ``` docker-compose -p d7 -f docker/docker-compose.yml up -d ``` +At this point, you should be able to access the UI from http://localhost:4000, +and the backend at http://localhost:8080/server/ + +## Run DSpace Angular dist build with DSpace Demo site backend + +This allows you to run the Angular UI in *production* mode, pointing it at the demo or sandbox backend +(https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/). + +``` +docker-compose -f docker/docker-compose-dist.yml pull +docker-compose -f docker/docker-compose-dist.yml build +docker-compose -p d7 -f docker/docker-compose-dist.yml up -d +``` + ## Ingest test data from AIPDIR Create an administrator ``` -docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en +docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e dspace.admin.dev@dataquest.sk -f admin -l user -p admin -c en ``` Load content from AIP files @@ -85,9 +135,10 @@ Load assetstore content and trigger a re-index of the repository docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli ``` -## End to end testing of the rest api (runs in travis). -_In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._ +## End to end testing of the REST API (runs in GitHub Actions CI). +_In this instance, only the REST api runs in Docker using the Entities dataset. GitHub Actions will perform CI testing of Angular using Node to drive the tests. See `.github/workflows/build.yml` for more details._ +This command is only really useful for testing our Continuous Integration process. ``` -docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d +docker-compose -p d7ci -f docker/docker-compose-ci.yml up -d ``` diff --git a/docker/cli.assetstore.yml b/docker/cli.assetstore.yml index 40e4974c7c7..c120243a337 100644 --- a/docker/cli.assetstore.yml +++ b/docker/cli.assetstore.yml @@ -12,15 +12,12 @@ # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/cli.assetstore.yml # # Therefore, it should be kept in sync with that file -version: "3.7" networks: dspacenet: services: dspace-cli: - networks: - dspacenet: {} environment: # This assetstore zip is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data - LOADASSETS=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/assetstore.tar.gz diff --git a/docker/cli.ingest.yml b/docker/cli.ingest.yml index 1db241af3bf..007309545bc 100644 --- a/docker/cli.ingest.yml +++ b/docker/cli.ingest.yml @@ -12,13 +12,12 @@ # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/cli.ingest.yml # # Therefore, it should be kept in sync with that file -version: "3.7" services: dspace-cli: environment: - AIPZIP=https://github.com/DSpace-Labs/AIP-Files/raw/main/dogAndReport.zip - - ADMIN_EMAIL=test@test.edu + - ADMIN_EMAIL=dspace.admin.dev@dataquest.sk - AIPDIR=/tmp/aip-dir entrypoint: - /bin/bash diff --git a/docker/cli.yml b/docker/cli.yml index 54b83d45036..e9758830e49 100644 --- a/docker/cli.yml +++ b/docker/cli.yml @@ -12,34 +12,55 @@ # https://github.com/DSpace/DSpace/blob/main/docker-compose-cli.yml # # Therefore, it should be kept in sync with that file -version: "3.7" - +networks: + # Default to using network named 'dspacenet' from docker-compose-rest.yml. + # Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet") + # If COMPOSITE_PROJECT_NAME is missing, default value will be "docker" (name of folder this file is in) + default: + name: ${COMPOSE_PROJECT_NAME:-docker}_dspacenet + external: true services: dspace-cli: - image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}" + image: "${DOCKER_OWNER:-dataquest}/dspace-cli:${DSPACE_VER:-dspace-7_x}" container_name: dspace-cli environment: + TZ: ${TIMEZONE:-Europe/Bratislava} # Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. # See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml # __P__ => "." (e.g. dspace__P__dir => dspace.dir) # __D__ => "-" (e.g. google__D__metadata => google-metadata) # dspace.dir dspace__P__dir: /dspace + dspace__P__server__P__url: ${REST_URL:-http://127.0.0.1:8080/server} + dspace__P__ui__P__url: ${UI_URL:-http://127.0.0.1:4000} + dspace__P__name: 'DSpace Started with Docker Compose' # db.url: Ensure we are using the 'dspacedb' image for our database db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' # solr.server: Ensure we are using the 'dspacesolr' image for Solr + solr__P__server: http://dspacesolr:8983/solr + + # S3 + assetstore__P__index__P__primary: ${S3_STORAGE:-0} + assetstore__P__s3__P__enabled: ${S3_ENABLED:-false} + assetstore__P__s3__P__useRelativePath: ${S3_RELATIVE_PATH:-false} + assetstore__P__s3__P__bucketName: ${S3_BUCKET:-bucket-for-dspace} + assetstore__P__s3__P__subfolder: ${S3_SUBFOLDER:-} + assetstore__P__s3__P__awsAccessKey: ${S3_ACCESS:-} + assetstore__P__s3__P__awsSecretKey: ${S3_SECRET:-} + assetstore__P__s3__P__awsRegionName: ${S3_REGION_NAME:-} + + assetstore__P__s3__P__pathStyleAccessEnabled: ${S3_PATH_STYLE_ACCESS:-false} + assetstore__P__s3__P__endpoint: ${S3_ENDPOINT:-} volumes: - "assetstore:/dspace/assetstore" + - dspace_cli_logs:/dspace/log + - ./local.cfg:/dspace/config/local.cfg entrypoint: /dspace/bin/dspace command: help - networks: - - dspacenet tty: true stdin_open: true volumes: assetstore: - -networks: - dspacenet: + dspace_cli_logs: diff --git a/docker/config.prod.yml b/docker/config.prod.yml new file mode 100644 index 00000000000..07a9ea5366a --- /dev/null +++ b/docker/config.prod.yml @@ -0,0 +1,3 @@ +# This is a sample config file for frontend. +# Replace with production config.prod.yml. +# It will be mounted to docker container. diff --git a/docker/db.entities.yml b/docker/db.entities.yml index d1dfdf4a26c..cc5620423e2 100644 --- a/docker/db.entities.yml +++ b/docker/db.entities.yml @@ -12,11 +12,9 @@ # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml # # # Therefore, it should be kept in sync with that file -version: "3.7" - services: dspacedb: - image: dspace/dspace-postgres-pgcrypto:loadsql + image: ${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}-loadsql environment: # This LOADSQL should be kept in sync with the URL in DSpace/DSpace # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data @@ -25,7 +23,7 @@ services: ### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' #### # Ensure that the database is ready BEFORE starting tomcat # 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep - # 2. Then, run database migration to init database tables + # 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any) # 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml # This 'sed' command inserts the sample configurations specific to the Entities data set, see: # https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49 @@ -35,7 +33,7 @@ services: - '-c' - | while (! /dev/null 2>&1; do sleep 1; done; - /dspace/bin/dspace database migrate + /dspace/bin/dspace database migrate ignored sed -i '/name-map collection-handle="default".*/a \\n \ \ \ @@ -48,4 +46,4 @@ services: \ \ ' /dspace/config/item-submission.xml - catalina.sh run \ No newline at end of file + catalina.sh run diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index 3bd8f526303..b81366e6861 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -10,7 +10,6 @@ # This is used by our GitHub CI at .github/workflows/build.yml # It is based heavily on the Backend's Docker Compose: # https://github.com/DSpace/DSpace/blob/main/docker-compose.yml -version: '3.7' networks: dspacenet: services: @@ -24,17 +23,21 @@ services: # __D__ => "-" (e.g. google__D__metadata => google-metadata) # dspace.dir, dspace.server.url and dspace.ui.url dspace__P__dir: /dspace - dspace__P__server__P__url: http://localhost:8080/server - dspace__P__ui__P__url: http://localhost:4000 + dspace__P__server__P__url: ${REST_URL:-http://127.0.0.1:8080/server} + dspace__P__ui__P__url: ${UI_URL:-http://127.0.0.1:4000} # db.url: Ensure we are using the 'dspacedb' image for our database db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' # solr.server: Ensure we are using the 'dspacesolr' image for Solr solr__P__server: http://dspacesolr:8983/solr + # Tell Statistics to commit all views immediately instead of waiting on Solr's autocommit. + # This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly. + solr__D__statistics__P__autoCommit: 'false' + LOGGING_CONFIG: /dspace/config/log4j2-container.xml depends_on: - dspacedb - image: dspace/dspace:dspace-7_x-test + image: ${DSPACE_CI_IMAGE:-dataquest/dspace:dspace-7_x-test} networks: - dspacenet: + - dspacenet ports: - published: 8080 target: 8080 @@ -42,46 +45,49 @@ services: tty: true volumes: - assetstore:/dspace/assetstore - # Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below) - - solr_configs:/dspace/solr # Ensure that the database is ready BEFORE starting tomcat # 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep - # 2. Then, run database migration to init database tables + # 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any) # 3. Finally, start Tomcat entrypoint: - /bin/bash - '-c' - | while (! /dev/null 2>&1; do sleep 1; done; - /dspace/bin/dspace database migrate + /dspace/bin/dspace database migrate force + /dspace/bin/dspace index-discovery -b catalina.sh run # DSpace database container # NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data dspacedb: container_name: dspacedb + image: ${DSPACE_DB_IMAGE:-dspace/dspace-postgres-pgcrypto:loadsql} environment: # This LOADSQL should be kept in sync with the LOADSQL in # https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml # This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data - LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql + LOADSQL: https://github.com/dataquest-dev/DSpace/releases/download/data/dspace-test-database-dump_29.1.2024.sql PGDATA: /pgdata - image: dspace/dspace-postgres-pgcrypto:loadsql + POSTGRES_PASSWORD: dspace networks: - dspacenet: + - dspacenet + ports: + - published: 5432 + target: 5432 stdin_open: true tty: true volumes: + # Keep Postgres data directory between reboots - pgdata:/pgdata # DSpace Solr container dspacesolr: container_name: dspacesolr - # Uses official Solr image at https://hub.docker.com/_/solr/ - image: solr:8.11-slim + image: ${DSPACE_SOLR_IMAGE:-dataquest/dspace-solr:dspace-7_x} # Needs main 'dspace' container to start first to guarantee access to solr_configs depends_on: - dspace networks: - dspacenet: + - dspacenet ports: - published: 8983 target: 8983 @@ -89,9 +95,6 @@ services: tty: true working_dir: /var/solr/data volumes: - # Mount our "solr_configs" volume available under the Solr's configsets folder (in a 'dspace' subfolder) - # This copies the Solr configs from main 'dspace' container into 'dspacesolr' via that volume - - solr_configs:/opt/solr/server/solr/configsets/dspace # Keep Solr data directory between reboots - solr_data:/var/solr/data # Initialize all DSpace Solr cores using the mounted configsets (see above), then start Solr @@ -100,14 +103,16 @@ services: - '-c' - | init-var-solr - precreate-core authority /opt/solr/server/solr/configsets/dspace/authority - precreate-core oai /opt/solr/server/solr/configsets/dspace/oai - precreate-core search /opt/solr/server/solr/configsets/dspace/search - precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics + precreate-core authority /opt/solr/server/solr/configsets/authority + cp -r /opt/solr/server/solr/configsets/authority/* authority + precreate-core oai /opt/solr/server/solr/configsets/oai + cp -r /opt/solr/server/solr/configsets/oai/* oai + precreate-core search /opt/solr/server/solr/configsets/search + cp -r /opt/solr/server/solr/configsets/search/* search + precreate-core statistics /opt/solr/server/solr/configsets/statistics + cp -r /opt/solr/server/solr/configsets/statistics/* statistics exec solr -f volumes: assetstore: pgdata: solr_data: - # Special volume used to share Solr configs from 'dspace' to 'dspacesolr' container (see above) - solr_configs: \ No newline at end of file diff --git a/docker/docker-compose-dist.yml b/docker/docker-compose-dist.yml new file mode 100644 index 00000000000..88e5be16a5d --- /dev/null +++ b/docker/docker-compose-dist.yml @@ -0,0 +1,39 @@ +# +# The contents of this file are subject to the license and copyright +# detailed in the LICENSE and NOTICE files at the root of the source +# tree and available online at +# +# http://www.dspace.org/license/ +# + +# Docker Compose for running the DSpace Angular UI dist build +# for previewing with the DSpace Demo site backend +networks: + dspacenet: +services: + dspace-angular: + container_name: dspace-angular + environment: + DSPACE_UI_SSL: 'false' + DSPACE_UI_HOST: dspace-angular + DSPACE_UI_PORT: '4000' + DSPACE_UI_NAMESPACE: / + # NOTE: When running the UI in production mode (which the -dist image does), + # these DSPACE_REST_* variables MUST point at a public, HTTPS URL. + # This is because Server Side Rendering (SSR) currently requires a public URL, + # see this bug: https://github.com/DSpace/dspace-angular/issues/1485 + DSPACE_REST_SSL: 'true' + DSPACE_REST_HOST: demo.dspace.org + DSPACE_REST_PORT: 443 + DSPACE_REST_NAMESPACE: /server + image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-7_x}-dist" + build: + context: .. + dockerfile: Dockerfile.dist + networks: + dspacenet: + ports: + - published: 4000 + target: 4000 + stdin_open: true + tty: true diff --git a/docker/docker-compose-mac.yml b/docker/docker-compose-mac.yml new file mode 100644 index 00000000000..664900fe80e --- /dev/null +++ b/docker/docker-compose-mac.yml @@ -0,0 +1,11 @@ +services: + dspacesolr: + platform: linux/amd64 + dspacedb: + platform: linux/amd64 + dspace: + platform: linux/amd64 + dspace-angular: + platform: linux/amd64 + dspace-cli: + platform: linux/amd64 diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index b73f1b7a390..bade0ecf24a 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -10,118 +10,178 @@ # This is based heavily on the docker-compose.yml that is available in the DSpace/DSpace # (Backend) at: # https://github.com/DSpace/DSpace/blob/main/docker-compose.yml -version: '3.7' networks: dspacenet: + # Due to the following specification, THIS FILE (docker-compose-rest.yml) must be last (if using several YMLs), + # since it specifies network in more detail. If it is not last, there is "root must be a mapping" error. ipam: config: # Define a custom subnet for our DSpace network, so that we can easily trust requests from host to container. # If you customize this value, be sure to customize the 'proxies.trusted.ipranges' env variable below. - - subnet: 172.23.0.0/16 + - subnet: 172.2${INSTANCE}.0.0/16 services: # DSpace (backend) webapp container dspace: - container_name: dspace + restart: unless-stopped + container_name: dspace${INSTANCE} environment: + JAVA_OPTS: ${JAVA_OPTS:--Xmx4g} + TZ: ${TIMEZONE:-Europe/Bratislava} # Below syntax may look odd, but it is how to override dspace.cfg settings via env variables. # See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml # __P__ => "." (e.g. dspace__P__dir => dspace.dir) # __D__ => "-" (e.g. google__D__metadata => google-metadata) # dspace.dir, dspace.server.url, dspace.ui.url and dspace.name dspace__P__dir: /dspace - dspace__P__server__P__url: http://localhost:8080/server - dspace__P__ui__P__url: http://localhost:4000 - dspace__P__name: 'DSpace Started with Docker Compose' + dspace__P__server__P__url: ${REST_URL:-http://127.0.0.1:8080/server} + dspace__P__ui__P__url: ${UI_URL:-http://127.0.0.1:4000} # db.url: Ensure we are using the 'dspacedb' image for our database db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace' # solr.server: Ensure we are using the 'dspacesolr' image for Solr solr__P__server: http://dspacesolr:8983/solr - # proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests + # proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests # from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above. - proxies__P__trusted__P__ipranges: '172.23.0' - image: dspace/dspace:dspace-7_x-test + proxies__P__trusted__P__ipranges: '172.2${INSTANCE}.0' + #S3 config + assetstore__P__index__P__primary: ${S3_STORAGE:-0} + assetstore__P__s3__P__enabled: ${S3_ENABLED:-false} + assetstore__P__s3__P__useRelativePath: ${S3_RELATIVE_PATH:-false} + assetstore__P__s3__P__bucketName: ${S3_BUCKET:-bucket-for-dspace} + assetstore__P__s3__P__subfolder: ${S3_SUBFOLDER:-} + assetstore__P__s3__P__awsAccessKey: ${S3_ACCESS:-} + assetstore__P__s3__P__awsSecretKey: ${S3_SECRET:-} + assetstore__P__s3__P__awsRegionName: ${S3_REGION_NAME:-} + assetstore__P__s3__P__pathStyleAccessEnabled: ${S3_PATH_STYLE_ACCESS:-false} + assetstore__P__s3__P__endpoint: ${S3_ENDPOINT:-} + image: ${DSPACE_REST_IMAGE:-dataquest/dspace:dtq-dev-7.5} depends_on: - dspacedb networks: - dspacenet: + - dspacenet ports: - - published: 8080 + # BE server port + - published: 808${INSTANCE} target: 8080 + host_ip: ${HOST_IP:-127.0.0.1} + # original debug port + - published: 800${INSTANCE} + target: 8000 + host_ip: ${HOST_IP:-127.0.0.1} + # handle binary port as per https://www.handle.net/hnr_support.html (5th paragraph) + - published: 264${INSTANCE} + target: 2641 + protocol: tcp + host_ip: ${HOST_IP:-127.0.0.1} + - published: 264${INSTANCE} + target: 2641 + protocol: udp + host_ip: ${HOST_IP:-127.0.0.1} + # handle http port as per https://www.handle.net/hnr_support.html (5th paragraph) + - published: 801${INSTANCE} + target: 8010 + host_ip: 127.0.0.1 stdin_open: true tty: true volumes: + - dspace_logs:/dspace/log - assetstore:/dspace/assetstore - # Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below) - - solr_configs:/dspace/solr + - handle_server:/dspace/handle-server + - ./local.cfg:/dspace/config/local.cfg + # Mount a tmpfs over the requests folder to protect it - associated with https://github.com/dataquest-dev/DSpace/pull/1095 +# - type: tmpfs +# target: /dspace/var/oai/requests +# tmpfs: +# size: 67108864 # 64 MB, adjust as needed +# mode: 1777 # writable, sticky bit # Ensure that the database is ready BEFORE starting tomcat # 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep # 2. Then, run database migration to init database tables - # 3. Finally, start Tomcat + # 3. Use custom_run script, to enable debugging under certain conditions + # 4. Start the Handle server!!!! (please make sure you DO actually want it, otherwise remove last line) entrypoint: - /bin/bash - '-c' + # When customizing the namespace, add the following command to the entrypoint command below (after `while ...`): + # `pushd ../webapps && (unlink server || true) && (ln -s /dspace/webapps/server/ 'repository#server' || true) && + # popd` + # The `(... || true)` condition is necessary to ensure the `popd` command runs at the end. + # It used to fail when the `server` folder did not exist in `/webapps`. + # This will create a symlink from the webapps directory to the server directory with the custom namespace + # (e.g. /dspace/webapps/server -> /dspace/webapps/repository#server) - | while (! /dev/null 2>&1; do sleep 1; done; - /dspace/bin/dspace database migrate - catalina.sh run - # DSpace database container + pushd /usr/local/tomcat/webapps && (unlink server || true) && (ln -s /dspace/webapps/server/ `echo -n "${DSPACE_REST_NAMESPACE}" | sed -e 's#^/##' -e 'sx/x#x'` || true) && popd + /dspace/bin/dspace database migrate force + /dspace/bin/start-handle-server + ./custom_run.sh + # DSpace database container dspacedb: - container_name: dspacedb + restart: unless-stopped + container_name: dspacedb${INSTANCE} environment: + TZ: ${TIMEZONE:-Europe/Bratislava} PGDATA: /pgdata - image: dspace/dspace-postgres-pgcrypto + POSTGRES_PASSWORD: dspace + image: ${DSPACE_DB_IMAGE:-dataquest/dspace-postgres-pgcrypto:dspace-7_x} networks: - dspacenet: + - dspacenet ports: - - published: 5432 + - published: 543${INSTANCE} target: 5432 + host_ip: ${HOST_IP:-127.0.0.1} stdin_open: true tty: true volumes: + # Keep Postgres data directory between reboots - pgdata:/pgdata - # DSpace Solr container + # Keep Postgres logs between reboots + - pg_logs:/var/log/postgresql + # DSpace Solr container dspacesolr: - container_name: dspacesolr - # Uses official Solr image at https://hub.docker.com/_/solr/ - image: solr:8.11-slim - # Needs main 'dspace' container to start first to guarantee access to solr_configs - depends_on: - - dspace + environment: + TZ: ${TIMEZONE:-Europe/Bratislava} + restart: unless-stopped + container_name: dspacesolr${INSTANCE} + image: ${DSPACE_SOLR_IMAGE:-dataquest/dspace-solr:dspace-7_x} networks: - dspacenet: + - dspacenet ports: - - published: 8983 + - published: 898${INSTANCE} target: 8983 + host_ip: ${HOST_IP:-127.0.0.1} stdin_open: true tty: true working_dir: /var/solr/data volumes: - # Mount our "solr_configs" volume available under the Solr's configsets folder (in a 'dspace' subfolder) - # This copies the Solr configs from main 'dspace' container into 'dspacesolr' via that volume - - solr_configs:/opt/solr/server/solr/configsets/dspace # Keep Solr data directory between reboots - solr_data:/var/solr/data + - solr_logs:/var/solr/logs # Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr # * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op - # * Second, copy updated configs from mounted configsets to this core. If it already existed, this updates core - # to the latest configs. If it's a newly created core, this is a no-op. + # * Second, copy configsets to this core: + # Updates to Solr configs require the container to be rebuilt/restarted: + # `docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr` entrypoint: - /bin/bash - '-c' - | init-var-solr - precreate-core authority /opt/solr/server/solr/configsets/dspace/authority - cp -r -u /opt/solr/server/solr/configsets/dspace/authority/* authority - precreate-core oai /opt/solr/server/solr/configsets/dspace/oai - cp -r -u /opt/solr/server/solr/configsets/dspace/oai/* oai - precreate-core search /opt/solr/server/solr/configsets/dspace/search - cp -r -u /opt/solr/server/solr/configsets/dspace/search/* search - precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics - cp -r -u /opt/solr/server/solr/configsets/dspace/statistics/* statistics - exec solr -f + precreate-core authority /opt/solr/server/solr/configsets/authority + cp -r /opt/solr/server/solr/configsets/authority/* authority + precreate-core oai /opt/solr/server/solr/configsets/oai + cp -r /opt/solr/server/solr/configsets/oai/* oai + precreate-core search /opt/solr/server/solr/configsets/search + cp -r /opt/solr/server/solr/configsets/search/* search + precreate-core statistics /opt/solr/server/solr/configsets/statistics + cp -r /opt/solr/server/solr/configsets/statistics/* statistics + exec solr -p 8983 -f -m 4g volumes: + # Commented out because there are a lot of files in the assetstore assetstore: pgdata: + pg_logs: solr_data: # Special volume used to share Solr configs from 'dspace' to 'dspacesolr' container (see above) - solr_configs: + dspace_logs: + solr_logs: + handle_server: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 1387b1de396..ea0e5f30f1b 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -9,31 +9,40 @@ # Docker Compose for running the DSpace Angular UI for testing/development # Requires also running a REST API backend (either locally or remotely), # for example via 'docker-compose-rest.yml' -version: '3.7' networks: dspacenet: services: dspace-angular: - container_name: dspace-angular + restart: unless-stopped + container_name: dspace-angular${INSTANCE} environment: + TZ: ${TIMEZONE:-Europe/Bratislava} DSPACE_UI_SSL: 'false' DSPACE_UI_HOST: dspace-angular - DSPACE_UI_PORT: '4000' - DSPACE_UI_NAMESPACE: / - DSPACE_REST_SSL: 'false' - DSPACE_REST_HOST: localhost - DSPACE_REST_PORT: 8080 - DSPACE_REST_NAMESPACE: /server - image: dspace/dspace-angular:dspace-7_x + # Use only `4000`, not the {UI_PORT} from the .env because in the container it is always `4000` + DSPACE_UI_PORT: 4000 + DSPACE_UI_NAMESPACE: ${DSPACE_UI_NAMESPACE:-/} + DSPACE_REST_SSL: ${DSPACE_SSL:-false} + DSPACE_REST_HOST: ${DSPACE_HOST:-localhost} + DSPACE_REST_PORT: ${DSPACE_REST_PORT:-8080} + DSPACE_REST_NAMESPACE: ${DSPACE_REST_NAMESPACE:-/server} + image: ${DSPACE_UI_IMAGE:-dataquest/dspace-angular:dspace-7_x} + volumes: + - ./config.prod.yml:/app/config/config.prod.yml +# - ./aai.js:/app/dist/browser/aai.js +# - ./dspace-ui.json:/app/docker/dspace-ui.json:rw build: context: .. dockerfile: Dockerfile networks: dspacenet: + entrypoint: ${FE_CMD:-/bin/sh -c "pm2-runtime start dspace-ui.json > /dev/null 2> /dev/null"} ports: - - published: 4000 + - published: 400${INSTANCE} target: 4000 - - published: 9876 - target: 9876 + host_ip: ${HOST_IP:-127.0.0.1} + - published: 987${INSTANCE} + target: 987${INSTANCE} + host_ip: ${HOST_IP:-127.0.0.1} stdin_open: true tty: true diff --git a/docker/dspace-ui.json b/docker/dspace-ui.json new file mode 100644 index 00000000000..fdd93daf66e --- /dev/null +++ b/docker/dspace-ui.json @@ -0,0 +1,15 @@ +{ + "apps": [ + { + "name": "dspace-ui", + "cwd": "/app", + "script": "dist/server/main.js", + "time": true, + "instances": "7", + "exec_mode": "cluster", + "node_args": "--max_old_space_size=1344", + "max_memory_restart": "2G", + "env": {"NODE_ENV": "production"} + } + ] +} diff --git a/docker/local.cfg b/docker/local.cfg new file mode 100644 index 00000000000..fc008e0f344 --- /dev/null +++ b/docker/local.cfg @@ -0,0 +1,3 @@ +# This is a sample config file for backend. +# Replace with production local.cfg. +# It will be mounted to docker container. diff --git a/docker/matomo-settings.ts b/docker/matomo-settings.ts new file mode 100644 index 00000000000..e0217cfb66b --- /dev/null +++ b/docker/matomo-settings.ts @@ -0,0 +1,7 @@ +/** + * Matomo settings for tracking statistics. This file could be mounted in the docker container. + */ +export const matomoSettings = { + hostUrl: 'http://localhost:8135/', + siteId: '1' +}; diff --git a/docker/matomo-w-db.yml b/docker/matomo-w-db.yml new file mode 100644 index 00000000000..1e7c23f6c94 --- /dev/null +++ b/docker/matomo-w-db.yml @@ -0,0 +1,36 @@ +services: + db: + image: mariadb + restart: always + ports: + - 127.0.0.1:3306:3306 + container_name: mdb + environment: + MARIADB_ROOT_PASSWORD: ${MATOMO_MARIADB_ROOT_PASSWORD:-example} + MARIADB_AUTO_UPGRADE: ${MATOMO_MARIADB_AUTO_UPGRADE:--1} + MARIADB_INITDB_SKIP_TZINFO: ${MATOMO_MARIADB_INITDB_SKIP_TZINFO:-1} + + gui: + image: phpmyadmin/phpmyadmin + ports: + - 8148:80 + container_name: matomo_phpAdmin + restart: always + links: + - "db:db" + + matomo: + image: matomo + container_name: matomo_statistics + restart: always + environment: + MYSQL_PASSWORD: ${MATOMO_MYSQL_PASSWORD:-example} + MYSQL_DATABASE: ${MATOMO_MYSQL_DATABASE:-matomo_statistics} + MYSQL_USER: ${MATOMO_MYSQL_USER:-root} + MATOMO_DATABASE_ADAPTER: mysql + MATOMO_DATABASE_HOST: db + MATOMO_DATABASE_USERNAME: ${MATOMO_DATABASE_USERNAME:-root} + MATOMO_DATABASE_PASSWORD: ${MATOMO_DATABASE_PASSWORD:-example} + MATOMO_DATABASE_DBNAME: ${MATOMO_DATABASE_DBNAME:-matomo_statistics} + ports: + - 8135:80 diff --git a/docs/Configuration.md b/docs/Configuration.md index 62fa444cc0f..01fd83c94d1 100644 --- a/docs/Configuration.md +++ b/docs/Configuration.md @@ -48,7 +48,7 @@ dspace-angular connects to your DSpace installation by using its REST endpoint. ```yaml rest: ssl: true - host: api7.dspace.org + host: demo.dspace.org port: 443 nameSpace: /server } @@ -57,7 +57,7 @@ rest: Alternately you can set the following environment variables. If any of these are set, it will override all configuration files: ``` DSPACE_REST_SSL=true - DSPACE_REST_HOST=api7.dspace.org + DSPACE_REST_HOST=demo.dspace.org DSPACE_REST_PORT=443 DSPACE_REST_NAMESPACE=/server ``` diff --git a/karma.conf.js b/karma.conf.js index 24cd067fd18..8418312b1ab 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -22,7 +22,7 @@ module.exports = function (config) { reports: ['html', 'lcovonly', 'text-summary'], fixWebpackSourcePaths: true }, - reporters: ['mocha', 'kjhtml'], + reporters: ['mocha', 'kjhtml', 'coverage-istanbul'], mochaReporter: { ignoreSkipped: true, output: 'autowatch' diff --git a/package.json b/package.json index 5e98af53dd6..565c0510999 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dspace-angular", - "version": "0.0.0", + "version": "7.6.5", "scripts": { "ng": "ng", "config:watch": "nodemon", @@ -9,19 +9,19 @@ "start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"", "start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr", "start:mirador:prod": "yarn run build:mirador && yarn run start:prod", + "preserve": "yarn base-href", "serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts", "serve:ssr": "node dist/server/main", - "analyze": "webpack-bundle-analyzer dist/browser/stats.json", - "build": "ng build", + "build": "ng build --configuration development", "build:stats": "ng build --stats-json", - "build:prod": "yarn run build:ssr", + "build:prod": "cross-env NODE_ENV=production yarn run build:ssr", "build:ssr": "ng build --configuration production && ng run dspace-angular:server:production", - "test": "ng test --sourceMap=true --watch=false --configuration test", - "test:watch": "nodemon --exec \"ng test --sourceMap=true --watch=true --configuration test\"", - "test:headless": "ng test --sourceMap=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", + "test": "ng test --source-map=true --watch=false --configuration test", + "test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"", + "test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage", "lint": "ng lint", "lint-fix": "ng lint --fix=true", - "e2e": "ng e2e", + "e2e": "cross-env NODE_ENV=production ng e2e", "clean:dev:config": "rimraf src/assets/config.json", "clean:coverage": "rimraf coverage", "clean:dist": "rimraf dist", @@ -29,14 +29,17 @@ "clean:log": "rimraf *.log*", "clean:json": "rimraf *.records.json", "clean:node": "rimraf node_modules", + "clean:cli": "rimraf .angular/cache", "clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json", - "clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:node", + "clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:cli && yarn run clean:node", "sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts", "build:mirador": "webpack --config webpack/webpack.mirador.config.ts", "merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts", "cypress:open": "cypress open", "cypress:run": "cypress run", - "env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts" + "env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts", + "base-href": "ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts", + "check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./" }, "browser": { "fs": false, @@ -46,149 +49,164 @@ }, "private": true, "resolutions": { - "minimist": "^1.2.5", - "webdriver-manager": "^12.1.8" + "@types/d3-dispatch": "3.0.6", + "ngx-mask": "13.1.15" }, "dependencies": { - "@angular/animations": "~11.2.14", - "@angular/cdk": "^11.2.13", - "@angular/common": "~11.2.14", - "@angular/compiler": "~11.2.14", - "@angular/core": "~11.2.14", - "@angular/forms": "~11.2.14", - "@angular/localize": "11.2.14", - "@angular/platform-browser": "~11.2.14", - "@angular/platform-browser-dynamic": "~11.2.14", - "@angular/platform-server": "~11.2.14", - "@angular/router": "~11.2.14", - "@kolkov/ngx-gallery": "^1.2.3", - "@ng-bootstrap/ng-bootstrap": "9.1.3", - "@ng-dynamic-forms/core": "^13.0.0", - "@ng-dynamic-forms/ui-ng-bootstrap": "^13.0.0", - "@ngrx/effects": "^11.1.1", - "@ngrx/router-store": "^11.1.1", - "@ngrx/store": "^11.1.1", - "@nguniversal/express-engine": "11.2.1", - "@ngx-translate/core": "^13.0.0", - "@nicky-lenaers/ngx-scroll-to": "^9.0.0", + "@angular/animations": "^15.2.10", + "@angular/cdk": "^15.2.9", + "@angular/common": "^15.2.10", + "@angular/compiler": "^15.2.10", + "@angular/core": "^15.2.10", + "@angular/forms": "^15.2.10", + "@angular/localize": "15.2.10", + "@angular/platform-browser": "^15.2.10", + "@angular/platform-browser-dynamic": "^15.2.10", + "@angular/platform-server": "^15.2.10", + "@angular/router": "^15.2.10", + "@babel/runtime": "7.27.6", + "@kolkov/ngx-gallery": "^2.0.1", + "@ng-bootstrap/ng-bootstrap": "^11.0.0", + "@ng-dynamic-forms/core": "^15.0.0", + "@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0", + "@ngrx/effects": "^15.4.0", + "@ngrx/router-store": "^15.4.0", + "@ngrx/store": "^15.4.0", + "@nguniversal/express-engine": "^15.2.1", + "@ngx-translate/core": "^14.0.0", + "@nth-cloud/ng-toggle": "11.0.0", + "@nicky-lenaers/ngx-scroll-to": "^14.0.0", "angular-idle-preload": "3.0.0", - "angular2-text-mask": "9.0.0", - "angulartics2": "^10.0.0", - "bootstrap": "4.3.1", - "caniuse-lite": "^1.0.30001165", + "angulartics2": "^12.2.1", + "axios": "^1.10.0", + "bootstrap": "^4.6.1", "cerialize": "0.1.18", - "cli-progress": "^3.8.0", - "compression": "^1.7.4", - "cookie-parser": "1.4.5", - "core-js": "^3.7.0", - "deepmerge": "^4.2.2", - "express": "^4.17.1", + "chart.js": "4.3.3", + "cli-progress": "^3.12.0", + "colors": "^1.4.0", + "compression": "^1.8.1", + "ng2-charts": "4.1.1", + "chart.js": "4.3.3", + "cookie-parser": "1.4.7", + "core-js": "^3.42.0", + "date-fns": "^2.30.0", + "d3": "^7.9.0", + "date-fns-tz": "^1.3.7", + "deepmerge": "^4.3.1", + "ejs": "^3.1.10", + "express": "^4.21.2", "express-rate-limit": "^5.1.3", - "fast-json-patch": "^3.0.0-1", - "file-saver": "^2.0.5", + "fast-json-patch": "^3.1.1", "filesize": "^6.1.0", - "font-awesome": "4.7.0", - "http-proxy-middleware": "^1.0.5", - "https": "1.0.0", + "http-proxy-middleware": "^2.0.9", + "http-terminator": "^3.2.0", + "isbot": "^5.1.28", "js-cookie": "2.2.1", "js-yaml": "^4.1.0", - "json5": "^2.1.3", - "jsonschema": "1.4.0", + "json5": "^2.2.3", + "jsonschema": "1.5.0", "jwt-decode": "^3.1.2", - "klaro": "^0.7.10", + "klaro": "^0.7.21", + "lindat-common": "^1.5.0", "lodash": "^4.17.21", - "mirador": "^3.3.0", + "lru-cache": "^7.14.1", + "markdown-it": "^13.0.2", + "markdown-it-mathjax3": "^4.3.2", + "mirador": "^3.4.3", "mirador-dl-plugin": "^0.13.0", - "mirador-share-plugin": "^0.11.0", - "moment": "^2.29.1", - "morgan": "^1.10.0", - "ng-mocks": "11.11.2", + "mirador-share-plugin": "^0.16.0", + "morgan": "^1.10.1", + "ng2-charts": "4.1.1", "ng2-file-upload": "1.4.0", - "ng2-nouislider": "^1.8.3", - "ngx-infinite-scroll": "^10.0.1", - "ngx-moment": "^5.0.0", - "ngx-pagination": "5.0.0", + "ng2-nouislider": "^2.0.0", + "ngx-infinite-scroll": "^15.0.0", + "ngx-pagination": "6.0.3", + "ngx-skeleton-loader": "^7.0.0", "ngx-sortablejs": "^11.1.0", - "nouislider": "^14.6.3", - "pem": "1.14.4", - "postcss-cli": "^8.3.0", - "reflect-metadata": "^0.1.13", - "rxjs": "^6.6.3", - "sortablejs": "1.13.0", - "tslib": "^2.0.0", - "url-parse": "^1.5.6", + "ngx-ui-switch": "^14.1.0", + "nouislider": "^15.8.1", + "pem": "1.14.8", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.2", + "sanitize-html": "^2.17.0", + "sortablejs": "1.15.6", "uuid": "^8.3.2", - "webfontloader": "1.6.28", - "zone.js": "^0.10.3" + "zone.js": "~0.13.3" }, "devDependencies": { - "@angular-builders/custom-webpack": "10.0.1", - "@angular-devkit/build-angular": "~0.1102.15", - "@angular/cli": "~11.2.15", - "@angular/compiler-cli": "~11.2.14", - "@angular/language-service": "~11.2.14", + "@angular-builders/custom-webpack": "~15.0.0", + "@angular-devkit/build-angular": "^15.2.11", + "@angular-eslint/builder": "15.2.1", + "@angular-eslint/eslint-plugin": "15.2.1", + "@angular-eslint/eslint-plugin-template": "15.2.1", + "@angular-eslint/schematics": "15.2.1", + "@angular-eslint/template-parser": "15.2.1", + "@angular/cli": "^16.2.16", + "@angular/compiler-cli": "^15.2.10", + "@angular/language-service": "^15.2.10", "@cypress/schematic": "^1.5.0", - "@fortawesome/fontawesome-free": "^5.5.0", - "@ngrx/store-devtools": "^11.1.1", - "@ngtools/webpack": "10.2.3", - "@nguniversal/builders": "~11.2.1", - "@types/deep-freeze": "0.1.2", - "@types/express": "^4.17.9", - "@types/file-saver": "^2.0.1", + "@fortawesome/fontawesome-free": "^6.7.2", + "@material-ui/core": "^4.12.4", + "@material-ui/icons": "^4.11.3", + "@ngrx/store-devtools": "^15.4.0", + "@ngtools/webpack": "^15.2.6", + "@nguniversal/builders": "^15.2.1", + "@types/deep-freeze": "0.1.5", + "@types/ejs": "^3.1.5", + "@types/express": "^4.17.21", + "@types/grecaptcha": "^3.0.9", + "@types/d3": "7.1.0", + "@types/d3-dispatch": "3.0.6", "@types/jasmine": "~3.6.0", - "@types/jasminewd2": "~2.0.8", "@types/js-cookie": "2.2.6", - "@types/lodash": "^4.14.165", - "@types/node": "^14.14.9", - "axe-core": "^4.3.3", - "codelyzer": "^6.0.0", - "compression-webpack-plugin": "^3.0.1", + "@types/lodash": "^4.17.17", + "@types/node": "^14.18.63", + "@types/sanitize-html": "^2.16.0", + "@typescript-eslint/eslint-plugin": "^5.62.0", + "@typescript-eslint/parser": "^5.62.0", + "axe-core": "^4.10.3", + "compression-webpack-plugin": "^9.2.0", "copy-webpack-plugin": "^6.4.1", "cross-env": "^7.0.3", - "css-loader": "3.4.0", - "cssnano": "^4.1.10", - "cypress": "9.5.1", - "cypress-axe": "^0.13.0", - "debug-loader": "^0.0.1", + "csstype": "^3.1.3", + "cypress": "^13.17.0", + "cypress-axe": "^1.6.0", "deep-freeze": "0.0.1", - "dotenv": "^8.2.0", - "fork-ts-checker-webpack-plugin": "^6.0.3", - "html-loader": "^1.3.2", - "html-webpack-plugin": "^4.5.0", - "jasmine-core": "~3.6.0", - "jasmine-marbles": "0.6.0", - "jasmine-spec-reporter": "~5.0.0", - "karma": "^6.3.14", - "karma-chrome-launcher": "~3.1.0", - "karma-coverage-istanbul-reporter": "~3.0.2", + "eslint": "^8.39.0", + "eslint-plugin-deprecation": "^1.5.0", + "eslint-plugin-import": "^2.32.0", + "eslint-plugin-jsdoc": "^45.0.0", + "eslint-plugin-jsonc": "^2.20.1", + "eslint-plugin-lodash": "^7.4.0", + "eslint-plugin-unused-imports": "^2.0.0", + "express-static-gzip": "^2.2.0", + "jasmine-core": "^3.8.0", + "jasmine-marbles": "0.9.2", + "karma": "^6.4.4", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage-istanbul-reporter": "~3.0.3", "karma-jasmine": "~4.0.0", "karma-jasmine-html-reporter": "^1.5.0", "karma-mocha-reporter": "2.2.5", - "nodemon": "^2.0.15", - "optimize-css-assets-webpack-plugin": "^5.0.4", - "postcss-apply": "0.11.0", - "postcss-import": "^12.0.1", - "postcss-loader": "^3.0.0", - "postcss-preset-env": "6.7.0", - "postcss-responsive-type": "1.0.0", - "protractor": "^7.0.0", - "protractor-istanbul-plugin": "2.0.0", - "raw-loader": "0.5.1", + "ng-mocks": "^14.13.5", + "ngx-mask": "~13.1.7", + "nodemon": "^2.0.22", + "postcss": "^8.5", + "postcss-import": "^14.0.0", + "postcss-loader": "^4.0.3", + "postcss-preset-env": "^7.4.2", + "prop-types": "^15.8.1", "react": "^16.14.0", + "react-copy-to-clipboard": "^5.1.0", "react-dom": "^16.14.0", "rimraf": "^3.0.2", - "rxjs-spy": "^7.5.3", - "sass-resources-loader": "^2.1.1", - "script-ext-html-webpack-plugin": "2.1.5", - "string-replace-loader": "^2.3.0", - "terser-webpack-plugin": "^2.3.1", - "ts-loader": "^5.2.0", + "sass": "~1.89.2", + "sass-loader": "^12.6.0", + "sass-resources-loader": "^2.2.5", "ts-node": "^8.10.2", - "tslint": "^6.1.3", - "typescript": "~4.0.5", - "webpack": "^4.44.2", - "webpack-bundle-analyzer": "^4.4.0", + "typescript": "~4.8.4", + "webpack": "5.76.1", "webpack-cli": "^4.2.0", - "webpack-dev-server": "^4.5.0" + "webpack-dev-server": "^4.15.2" } } diff --git a/postcss.config.js b/postcss.config.js index df092d1d39f..f8b9666b312 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,8 +1,6 @@ module.exports = { plugins: [ require('postcss-import')(), - require('postcss-preset-env')(), - require('postcss-apply')(), - require('postcss-responsive-type')() + require('postcss-preset-env')() ] }; diff --git a/scripts/base-href.ts b/scripts/base-href.ts new file mode 100644 index 00000000000..7212e1c5168 --- /dev/null +++ b/scripts/base-href.ts @@ -0,0 +1,36 @@ +import { existsSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +import { AppConfig } from '../src/config/app-config.interface'; +import { buildAppConfig } from '../src/config/config.server'; + +/** + * Script to set baseHref as `ui.nameSpace` for development mode. Adds `baseHref` to angular.json build options. + * + * Usage (see package.json): + * + * yarn base-href + */ + +const appConfig: AppConfig = buildAppConfig(); + +const angularJsonPath = join(process.cwd(), 'angular.json'); + +if (!existsSync(angularJsonPath)) { + console.error(`Error:\n${angularJsonPath} does not exist\n`); + process.exit(1); +} + +try { + const angularJson = require(angularJsonPath); + + const baseHref = `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`; + + console.log(`Setting baseHref to ${baseHref} in angular.json`); + + angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref; + + writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n'); +} catch (e) { + console.error(e); +} diff --git a/scripts/env-to-yaml.ts b/scripts/env-to-yaml.ts index c2dd1cf0cae..6e8153f4c11 100644 --- a/scripts/env-to-yaml.ts +++ b/scripts/env-to-yaml.ts @@ -1,5 +1,5 @@ -import * as fs from 'fs'; -import * as yaml from 'js-yaml'; +import { existsSync, writeFileSync } from 'fs'; +import { dump } from 'js-yaml'; import { join } from 'path'; /** @@ -18,7 +18,7 @@ if (args[0] === undefined) { const envFullPath = join(process.cwd(), args[0]); -if (!fs.existsSync(envFullPath)) { +if (!existsSync(envFullPath)) { console.error(`Error:\n${envFullPath} does not exist\n`); process.exit(1); } @@ -26,10 +26,10 @@ if (!fs.existsSync(envFullPath)) { try { const env = require(envFullPath).environment; - const config = yaml.dump(env); + const config = dump(env); if (args[1]) { const ymlFullPath = join(process.cwd(), args[1]); - fs.writeFileSync(ymlFullPath, config); + writeFileSync(ymlFullPath, config); } else { console.log(config); } diff --git a/scripts/language-translation-helper.py b/scripts/language-translation-helper.py new file mode 100644 index 00000000000..f0685418782 --- /dev/null +++ b/scripts/language-translation-helper.py @@ -0,0 +1,37 @@ +## This script could help to find out which messages should be translated. `en.json5` to `cs.json5`. +import csv +import json5 + +# txt_file_lines = ['{'] +txt_file_lines = [] +json_file_lines = [] +missing = [] + +# Open csv file with translated messages +# with open("filename.csv", encoding="utf8") as csvfile: +# csvreader = csv.reader(csvfile, delimiter=";") +# +# for name, en, cs in csvreader: +# if name == 'id': +# continue +# line1 = '\n' +# line2 = ' // \"' + name.strip() + '\"' + ' : ' + "\"" + en.strip() + "\"" + "\n" +# line3 = ' \"' + name.strip() + '\"' + ' : ' + "\"" + cs.strip() + "\"" + "," +# txt_file_lines.append(line1 + line2 + line3) +# txt_file_lines.append('}') + +# Load json5 file messages +# with open('en.json5', encoding="utf8") as f: +# json_p = json5.load(f) +# for name in json_p: +# json_file_lines.append(name) + +# Check missing messages based on the message name +# for name in cs_7_5_names: +# if name not in test_names: +# missing.append(name) + +# Store new messages into *.txt file which content will be copies into some *.json5 file +# with open('translated_tul_cl_json.txt', 'w', encoding='utf-8') as f: +# f.write('\n'.join(txt_file_lines)) + diff --git a/scripts/serve.ts b/scripts/serve.ts index bf5506b8bd0..ee8570a45c1 100644 --- a/scripts/serve.ts +++ b/scripts/serve.ts @@ -1,4 +1,4 @@ -import * as child from 'child_process'; +import { spawn } from 'child_process'; import { AppConfig } from '../src/config/app-config.interface'; import { buildAppConfig } from '../src/config/config.server'; @@ -7,8 +7,9 @@ const appConfig: AppConfig = buildAppConfig(); /** * Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl + * Any CLI arguments given to this script are patched through to `ng serve` as well. */ -child.spawn( - `ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl}`, +spawn( + `ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')} --configuration development`, { stdio: 'inherit', shell: true } ); diff --git a/scripts/sourceversion.py b/scripts/sourceversion.py new file mode 100644 index 00000000000..c6d50504613 --- /dev/null +++ b/scripts/sourceversion.py @@ -0,0 +1,38 @@ +import subprocess +import sys +from datetime import datetime, timezone + +# when next editing this script, please introduce argparse. +# do not forget, it is called in BE by .github\workflows\reusable-docker-build.yml +# argparse must be introduced there. +# that action also calls BE version of this script, which is different (BE: scripts/sourceversion.py). +# It must also cooperate with argparse + +# the idea is, that this will be different on each branch, but could be possibly passed by argv/argparse +RELEASE_TAG_BASE='none' + +def get_time_in_timezone(zone: str = "Europe/Bratislava"): + try: + from zoneinfo import ZoneInfo + my_tz = ZoneInfo(zone) + except Exception as e: + my_tz = timezone.utc + return datetime.now(my_tz) + + +if __name__ == '__main__': + ts = get_time_in_timezone() + # we have html tags, since this script ends up creating VERSION_D.html + print(f"

This info was generated on:
{ts.strftime('%Y-%m-%d %H:%M:%S %Z%z')}

") + + cmd = 'git log -1 --pretty=format:"

Git hash:
%H
Date of commit:
%ai

"' + subprocess.check_call(cmd, shell=True) + + # when adding argparse, this should be a bit more obvious + link = sys.argv[1] + sys.argv[2] + print('

Build run:

' + link + ' ') + + link = "https://github.com/dataquest-dev/dspace-angular/releases/tag/" \ + + RELEASE_TAG_BASE + "-" + datetime.now().strftime('%Y.%m.') + sys.argv[2] + + print('

Release link:

' + link + ' (if it does not work, then this is not an official release instance) ') diff --git a/scripts/sync-i18n-files.ts b/scripts/sync-i18n-files.ts old mode 100755 new mode 100644 index ad8a712f210..96ba0d40105 --- a/scripts/sync-i18n-files.ts +++ b/scripts/sync-i18n-files.ts @@ -1,4 +1,5 @@ -import { projectRoot} from '../webpack/helpers'; +import { projectRoot } from '../webpack/helpers'; + const commander = require('commander'); const fs = require('fs'); const JSON5 = require('json5'); @@ -119,7 +120,7 @@ function syncFileWithSource(pathToTargetFile, pathToOutputFile) { outputChunks.forEach(function (chunk) { progressBar.increment(); chunk.split("\n").forEach(function (line) { - file.write(" " + line + "\n"); + file.write((line === '' ? '' : ` ${line}`) + "\n"); }); }); file.write("\n}"); @@ -192,7 +193,10 @@ function createNewChunkComparingSourceAndTarget(correspondingTargetChunk, source const targetList = correspondingTargetChunk.split("\n"); const oldKeyValueInTargetComments = getSubStringWithRegex(correspondingTargetChunk, "\\s*\\/\\/\\s*\".*"); - const keyValueTarget = targetList[targetList.length - 1]; + let keyValueTarget = targetList[targetList.length - 1]; + if (!keyValueTarget.endsWith(",")) { + keyValueTarget = keyValueTarget + ","; + } if (oldKeyValueInTargetComments != null) { const oldKeyValueUncommented = getSubStringWithRegex(oldKeyValueInTargetComments[0], "\".*")[0]; diff --git a/scripts/test-rest.ts b/scripts/test-rest.ts index aa3b64f62b6..9066777c42a 100644 --- a/scripts/test-rest.ts +++ b/scripts/test-rest.ts @@ -1,9 +1,9 @@ -import * as http from 'http'; -import * as https from 'https'; +import { request } from 'http'; +import { request as https_request } from 'https'; import { AppConfig } from '../src/config/app-config.interface'; import { buildAppConfig } from '../src/config/config.server'; - + const appConfig: AppConfig = buildAppConfig(); /** @@ -20,9 +20,15 @@ console.log(`...Testing connection to REST API at ${restUrl}...\n`); // If SSL enabled, test via HTTPS, else via HTTP if (appConfig.rest.ssl) { - const req = https.request(restUrl, (res) => { + const req = https_request(restUrl, (res) => { console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`); - res.on('data', (data) => { + // We will keep reading data until the 'end' event fires. + // This ensures we don't just read the first chunk. + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { checkJSONResponse(data); }); }); @@ -33,9 +39,15 @@ if (appConfig.rest.ssl) { req.end(); } else { - const req = http.request(restUrl, (res) => { + const req = request(restUrl, (res) => { console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`); - res.on('data', (data) => { + // We will keep reading data until the 'end' event fires. + // This ensures we don't just read the first chunk. + let data = ''; + res.on('data', (chunk) => { + data += chunk; + }); + res.on('end', () => { checkJSONResponse(data); }); }); diff --git a/scripts/webpack.js b/scripts/webpack.js deleted file mode 100644 index 93f17b4619f..00000000000 --- a/scripts/webpack.js +++ /dev/null @@ -1,13 +0,0 @@ -const path = require('path'); -const child_process = require('child_process'); - -const heapSize = 4096; -const webpackPath = path.join('node_modules', 'webpack', 'bin', 'webpack.js'); - -const params = [ - '--max_old_space_size=' + heapSize, - webpackPath, - ...process.argv.slice(2) -]; - -child_process.spawn('node', params, { stdio:'inherit' }); diff --git a/server.ts b/server.ts index da3b877bc13..d92310746e9 100644 --- a/server.ts +++ b/server.ts @@ -15,21 +15,28 @@ * import for `ngExpressEngine`. */ -import 'zone.js/dist/zone-node'; +import 'zone.js/node'; import 'reflect-metadata'; import 'rxjs'; -import * as pem from 'pem'; -import * as https from 'https'; +/* eslint-disable import/no-namespace */ import * as morgan from 'morgan'; import * as express from 'express'; -import * as bodyParser from 'body-parser'; +import * as ejs from 'ejs'; import * as compression from 'compression'; - -import { existsSync, readFileSync } from 'fs'; +import * as expressStaticGzip from 'express-static-gzip'; +/* eslint-enable import/no-namespace */ +import axios from 'axios'; +import LRU from 'lru-cache'; +import { isbot } from 'isbot'; +import { createCertificate } from 'pem'; +import { createServer } from 'https'; +import { json } from 'body-parser'; +import { createHttpTerminator } from 'http-terminator'; + +import { readFileSync } from 'fs'; import { join } from 'path'; -import { APP_BASE_HREF } from '@angular/common'; import { enableProdMode } from '@angular/core'; import { ngExpressEngine } from '@nguniversal/express-engine'; @@ -37,15 +44,19 @@ import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { environment } from './src/environments/environment'; import { createProxyMiddleware } from 'http-proxy-middleware'; -import { hasValue, hasNoValue } from './src/app/shared/empty.util'; +import { hasNoValue, hasValue } from './src/app/shared/empty.util'; import { UIServerConfig } from './src/config/ui-server-config.interface'; import { ServerAppModule } from './src/main.server'; import { buildAppConfig } from './src/config/config.server'; -import { AppConfig, APP_CONFIG } from './src/config/app-config.interface'; +import { APP_CONFIG, AppConfig } from './src/config/app-config.interface'; import { extendEnvironmentWithAppConfig } from './src/config/config.util'; +import { logStartupMessage } from './startup-message'; +import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model'; +import { SsrExcludePatterns } from './src/config/universal-config.interface'; + /* * Set path for the browser application's dist folder @@ -54,31 +65,52 @@ const DIST_FOLDER = join(process.cwd(), 'dist/browser'); // Set path fir IIIF viewer. const IIIF_VIEWER = join(process.cwd(), 'dist/iiif'); -const indexHtml = existsSync(join(DIST_FOLDER, 'index.html')) ? 'index.html' : 'index'; +const indexHtml = join(DIST_FOLDER, 'index.html'); const cookieParser = require('cookie-parser'); const appConfig: AppConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json')); +// cache of SSR pages for known bots, only enabled in production mode +let botCache: LRU; + +// cache of SSR pages for anonymous users. Disabled by default, and only available in production mode +let anonymousCache: LRU; + // extend environment with app config for server extendEnvironmentWithAppConfig(environment, appConfig); +// The REST server base URL +const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl; + // The Express app is exported so that it can be used by serverless Functions. export function app() { + const router = express.Router(); + /* * Create a new express application */ const server = express(); + // Tell Express to trust X-FORWARDED-* headers from proxies + // See https://expressjs.com/en/guide/behind-proxies.html + server.set('trust proxy', environment.ui.useProxies); + /* * If production mode is enabled in the environment file: * - Enable Angular's production mode - * - Enable compression for response bodies. See [compression](https://github.com/expressjs/compression) + * - Initialize caching of SSR rendered pages (if enabled in config.yml) + * - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression) */ if (environment.production) { enableProdMode(); - server.use(compression()); + initCache(); + server.use(compression({ + // only compress responses we've marked as SSR + // otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin + filter: (_, res) => res.locals.ssr, + })); } /* @@ -89,20 +121,21 @@ export function app() { /* * Add cookie parser middleware - * See [morgan](https://github.com/expressjs/cookie-parser) + * See [cookie-parser](https://github.com/expressjs/cookie-parser) */ server.use(cookieParser()); /* - * Add parser for request bodies - * See [morgan](https://github.com/expressjs/body-parser) + * Add JSON parser for request bodies + * See [body-parser](https://github.com/expressjs/body-parser) */ - server.use(bodyParser.json()); + server.use(json()); // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) server.engine('html', (_, options, callback) => ngExpressEngine({ bootstrap: ServerAppModule, + inlineCriticalCss: environment.universal.inlineCriticalCss, providers: [ { provide: REQUEST, @@ -120,10 +153,23 @@ export function app() { })(_, (options as any), callback) ); + server.engine('ejs', ejs.renderFile); + /* * Register the view engines for html and ejs */ server.set('view engine', 'html'); + server.set('view engine', 'ejs'); + + /** + * Serve the robots.txt ejs template, filling in the origin variable + */ + server.get('/robots.txt', (req, res) => { + res.setHeader('content-type', 'text/plain'); + res.render('assets/robots.txt.ejs', { + 'origin': req.protocol + '://' + req.headers.host + }); + }); /* * Set views folder path to directory where template files are stored @@ -133,7 +179,20 @@ export function app() { /** * Proxy the sitemaps */ - server.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, changeOrigin: true })); + router.use('/sitemap**', createProxyMiddleware({ + target: `${REST_BASE_URL}/sitemaps`, + pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), + changeOrigin: true + })); + + /** + * Proxy the linksets + */ + router.use('/signposting**', createProxyMiddleware({ + target: `${REST_BASE_URL}`, + pathRewrite: path => path.replace(environment.ui.nameSpace, '/'), + changeOrigin: true + })); /** * Checks if the rateLimiter property is present @@ -150,15 +209,31 @@ export function app() { /* * Serve static resources (images, i18n messages, …) + * Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip) */ - server.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false })); + router.get('*.*', addCacheControl, expressStaticGzip(DIST_FOLDER, { + index: false, + enableBrotli: true, + orderPreference: ['br', 'gzip'], + })); + /* * Fallthrough to the IIIF viewer (must be included in the build). */ - server.use('/iiif', express.static(IIIF_VIEWER, {index:false})); + router.use('/iiif', express.static(IIIF_VIEWER, { index: false })); - // Register the ngApp callback function to handle incoming requests - server.get('*', ngApp); + /** + * Checking server status + */ + server.get('/app/health', healthCheck); + + /** + * Default sending all incoming requests to ngApp() function, after first checking for a cached + * copy of the page (see cacheCheck()) + */ + router.get('*', cacheCheck, ngApp); + + server.use(environment.ui.nameSpace, router); return server; } @@ -167,50 +242,300 @@ export function app() { * The callback function to serve server side angular */ function ngApp(req, res) { - if (environment.universal.preboot) { - res.render(indexHtml, { - req, - res, - preboot: environment.universal.preboot, - async: environment.universal.async, - time: environment.universal.time, - baseUrl: environment.ui.nameSpace, - originUrl: environment.ui.baseUrl, - requestUrl: req.originalUrl, - providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }] - }, (err, data) => { - if (hasNoValue(err) && hasValue(data)) { - res.send(data); - } else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') { - // When this error occurs we can't fall back to CSR because the response has already been - // sent. These errors occur for various reasons in universal, not all of which are in our - // control to solve. - console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client'); - } else { - console.warn('Error in SSR, serving for direct CSR.'); - if (hasValue(err)) { - console.warn('Error details : ', err); - } - res.sendFile(DIST_FOLDER + '/index.html'); - } - }); + if (environment.universal.preboot && req.method === 'GET' && (req.path === '/' || !isExcludedFromSsr(req.path, environment.universal.excludePathPatterns))) { + // Render the page to user via SSR (server side rendering) + serverSideRender(req, res); } else { // If preboot is disabled, just serve the client - console.log('Universal off, serving for direct CSR'); - res.sendFile(DIST_FOLDER + '/index.html'); + console.log('Universal off, serving for direct client-side rendering (CSR)'); + clientSideRender(req, res); } } +/** + * Render page content on server side using Angular SSR. By default this page content is + * returned to the user. + * @param req current request + * @param res current response + * @param sendToUser if true (default), send the rendered content to the user. + * If false, then only save this rendered content to the in-memory cache (to refresh cache). + */ +function serverSideRender(req, res, sendToUser: boolean = true) { + // Render the page via SSR (server side rendering) + res.render(indexHtml, { + req, + res, + preboot: environment.universal.preboot, + async: environment.universal.async, + time: environment.universal.time, + baseUrl: environment.ui.nameSpace, + originUrl: environment.ui.baseUrl, + requestUrl: req.originalUrl, + }, (err, data) => { + if (hasNoValue(err) && hasValue(data)) { + // Replace REST URL with UI URL + if (environment.universal.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) { + data = data.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl); + } + + // save server side rendered page to cache (if any are enabled) + saveToCache(req, data); + if (sendToUser) { + res.locals.ssr = true; // mark response as SSR (enables text compression) + // send rendered page to user + res.send(data); + } + } else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') { + // When this error occurs we can't fall back to CSR because the response has already been + // sent. These errors occur for various reasons in universal, not all of which are in our + // control to solve. + console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client'); + } else { + console.warn('Error in server-side rendering (SSR)'); + if (hasValue(err)) { + console.warn('Error details : ', err); + } + if (sendToUser) { + console.warn('Falling back to serving direct client-side rendering (CSR).'); + clientSideRender(req, res); + } + } + }); +} + +// Read file once at startup +const indexHtmlContent = readFileSync(indexHtml, 'utf8'); + +function clientSideRender(req, res) { + const namespace = environment.ui.nameSpace || '/'; + let html = indexHtmlContent; + // Replace base href dynamically + html = html.replace( + //, + `` + ); + + // Replace REST URL with UI URL + if (environment.universal.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) { + html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl); + } + + res.send(html); +} + + /* - * Adds a cache control header to the response - * The cache control value can be configured in the environments file and defaults to max-age=60 + * Adds a Cache-Control HTTP header to the response. + * The cache control value can be configured in the config.*.yml file + * Defaults to max-age=604,800 seconds (1 week) */ -function cacheControl(req, res, next) { +function addCacheControl(req, res, next) { // instruct browser to revalidate - res.header('Cache-Control', environment.cache.control || 'max-age=60'); + res.header('Cache-Control', environment.cache.control || 'max-age=604800'); next(); } +/* + * Initialize server-side caching of pages rendered via SSR. + */ +function initCache() { + if (botCacheEnabled()) { + // Initialize a new "least-recently-used" item cache (where least recently used pages are removed first) + // See https://www.npmjs.com/package/lru-cache + // When enabled, each page defaults to expiring after 1 day (defined in default-app-config.ts) + botCache = new LRU( { + max: environment.cache.serverSide.botCache.max, + ttl: environment.cache.serverSide.botCache.timeToLive, + allowStale: environment.cache.serverSide.botCache.allowStale + }); + } + + if (anonymousCacheEnabled()) { + // NOTE: While caches may share SSR pages, this cache must be kept separately because the timeToLive + // may expire pages more frequently. + // When enabled, each page defaults to expiring after 10 seconds (defined in default-app-config.ts) + // to minimize anonymous users seeing out-of-date content + anonymousCache = new LRU( { + max: environment.cache.serverSide.anonymousCache.max, + ttl: environment.cache.serverSide.anonymousCache.timeToLive, + allowStale: environment.cache.serverSide.anonymousCache.allowStale + }); + } +} + +/** + * Return whether bot-specific server side caching is enabled in configuration. + */ +function botCacheEnabled(): boolean { + // Caching is only enabled if SSR is enabled AND + // "max" pages to cache is greater than zero + return environment.universal.preboot && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0); +} + +/** + * Return whether anonymous user server side caching is enabled in configuration. + */ +function anonymousCacheEnabled(): boolean { + // Caching is only enabled if SSR is enabled AND + // "max" pages to cache is greater than zero + return environment.universal.preboot && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0); +} + +/** + * Check if the currently requested page is in our server-side, in-memory cache. + * Caching is ONLY done for SSR requests. Pages are cached base on their path (e.g. /home or /search?query=test) + */ +function cacheCheck(req, res, next) { + // Cached copy of page (if found) + let cachedCopy; + + // If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page. + if (botCacheEnabled() && isbot(req.get('user-agent'))) { + cachedCopy = checkCacheForRequest('bot', botCache, req, res); + } else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) { + cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res); + } + + // If cached copy exists, return it to the user. + if (cachedCopy && cachedCopy.page) { + if (cachedCopy.headers) { + Object.keys(cachedCopy.headers).forEach((header) => { + if (cachedCopy.headers[header]) { + if (environment.cache.serverSide.debug) { + console.log(`Restore cached ${header} header`); + } + res.setHeader(header, cachedCopy.headers[header]); + } + }); + } + res.locals.ssr = true; // mark response as SSR-generated (enables text compression) + res.send(cachedCopy.page); + + // Tell Express to skip all other handlers for this path + // This ensures we don't try to re-render the page since we've already returned the cached copy + next('router'); + } else { + // If nothing found in cache, just continue with next handler + // (This should send the request on to the handler that rerenders the page via SSR + next(); + } +} + +/** + * Checks if the current request (i.e. page) is found in the given cache. If it is found, + * the cached copy is returned. When found, this method also triggers a re-render via + * SSR if the cached copy is now expired (i.e. timeToLive has passed for this cached copy). + * @param cacheName name of cache (just useful for debug logging) + * @param cache LRU cache to check + * @param req current request to look for in the cache + * @param res current response + * @returns cached copy (if found) or undefined (if not found) + */ +function checkCacheForRequest(cacheName: string, cache: LRU, req, res): any { + // Get the cache key for this request + const key = getCacheKey(req); + + // Check if this page is in our cache + let cachedCopy = cache.get(key); + if (cachedCopy) { + if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); } + + // Check if cached copy is expired (If expired, the key will now be gone from cache) + // NOTE: This will only occur when "allowStale=true", as it means the "get(key)" above returned a stale value. + if (!cache.has(key)) { + if (environment.cache.serverSide.debug) { console.log(`CACHE EXPIRED FOR ${key} in ${cacheName} cache. Re-rendering...`); } + // Update cached copy by rerendering server-side + // NOTE: In this scenario the currently cached copy will be returned to the current user. + // This re-render is peformed behind the scenes to update cached copy for next user. + serverSideRender(req, res, false); + } + } else { + if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); } + } + + // return page from cache + return cachedCopy; +} + +/** + * Create a cache key from the current request. + * The cache key is the URL path (NOTE: this key will also include any querystring params). + * E.g. "/home" or "/search?query=test" + * @param req current request + * @returns cache key to use for this page + */ +function getCacheKey(req): string { + // NOTE: this will return the URL path *without* any baseUrl + return req.url; +} + +/** + * Save page to server side cache(s), if enabled. If caching is not enabled or a user is authenticated, this is a noop + * If multiple caches are enabled, the page will be saved to any caches where it does not yet exist (or is expired). + * (This minimizes the number of times we need to run SSR on the same page.) + * @param req current page request + * @param page page data to save to cache + */ +function saveToCache(req, page: any) { + // Only cache if no one is currently authenticated. This means ONLY public pages can be cached. + // NOTE: It's not safe to save page data to the cache when a user is authenticated. In that situation, + // the page may include sensitive or user-specific materials. As the cache is shared across all users, it can only contain public info. + if (!isUserAuthenticated(req)) { + const key = getCacheKey(req); + // Avoid caching "/reload/[random]" paths (these are hard refreshes after logout) + if (key.startsWith('/reload')) { return; } + // Avoid caching not successful responses (status code different from 2XX status) + if (hasNotSucceeded(req.res.statusCode)) { return; } + + // Retrieve response headers to save, if any + const headers = retrieveHeaders(req.res); + // If bot cache is enabled, save it to that cache if it doesn't exist or is expired + // (NOTE: has() will return false if page is expired in cache) + if (botCacheEnabled() && !botCache.has(key)) { + botCache.set(key, { page, headers }); + if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in bot cache.`); } + } + + // If anonymous cache is enabled, save it to that cache if it doesn't exist or is expired + if (anonymousCacheEnabled() && !anonymousCache.has(key)) { + anonymousCache.set(key, { page, headers }); + if (environment.cache.serverSide.debug) { console.log(`CACHE SAVE FOR ${key} in anonymous cache.`); } + } + } +} + +/** + * Check if status code is different from 2XX + * @param statusCode + */ +function hasNotSucceeded(statusCode) { + const rgx = new RegExp(/^20+/); + return !rgx.test(statusCode); +} + +function retrieveHeaders(response) { + const headers = Object.create({}); + if (Array.isArray(environment.cache.serverSide.headers) && environment.cache.serverSide.headers.length > 0) { + environment.cache.serverSide.headers.forEach((header) => { + if (response.hasHeader(header)) { + if (environment.cache.serverSide.debug) { + console.log(`Save ${header} header to cache`); + } + headers[header] = response.getHeader(header); + } + }); + } + + return headers; +} +/** + * Whether a user is authenticated or not + */ +function isUserAuthenticated(req): boolean { + // Check whether our DSpace authentication Cookie exists or not + return req.cookies[TOKENITEM]; +} + /* * Callback function for when the server has started */ @@ -223,26 +548,51 @@ function serverStarted() { * @param keys SSL credentials */ function createHttpsServer(keys) { - https.createServer({ + const listener = createServer({ key: keys.serviceKey, cert: keys.certificate }, app).listen(environment.ui.port, environment.ui.host, () => { serverStarted(); }); + + // Graceful shutdown when signalled + const terminator = createHttpTerminator({server: listener}); + process.on('SIGINT', () => { + void (async ()=> { + console.debug('Closing HTTPS server on signal'); + await terminator.terminate().catch(e => { console.error(e); }); + console.debug('HTTPS server closed'); + })(); + }); } +/** + * Create an HTTP server with the configured port and host. + */ function run() { const port = environment.ui.port || 4000; const host = environment.ui.host || '/'; // Start up the Node server const server = app(); - server.listen(port, host, () => { + const listener = server.listen(port, host, () => { serverStarted(); }); + + // Graceful shutdown when signalled + const terminator = createHttpTerminator({server: listener}); + process.on('SIGINT', () => { + void (async () => { + console.debug('Closing HTTP server on signal'); + await terminator.terminate().catch(e => { console.error(e); }); + console.debug('HTTP server closed.');return undefined; + })(); + }); } function start() { + logStartupMessage(environment); + /* * If SSL is enabled * - Read credentials from configuration files @@ -275,7 +625,7 @@ function start() { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // lgtm[js/disabling-certificate-validation] - pem.createCertificate({ + createCertificate({ days: 1, selfSigned: true }, (error, keys) => { @@ -287,6 +637,36 @@ function start() { } } +/** + * Check if SSR should be skipped for path + * + * @param path + * @param excludePathPattern + */ +function isExcludedFromSsr(path: string, excludePathPattern: SsrExcludePatterns[]): boolean { + const patterns = excludePathPattern.map(p => + new RegExp(p.pattern, p.flag || '') + ); + return patterns.some((regex) => { + return regex.test(path) + }); +} + +/* + * The callback function to serve health check requests + */ +function healthCheck(req, res) { + const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`; + axios.get(baseUrl) + .then((response) => { + res.status(response.status).send(response.data); + }) + .catch((error) => { + res.status(error.response.status).send({ + error: error.message + }); + }); +} // Webpack will replace 'require' with '__webpack_require__' // '__non_webpack_require__' is a proxy to Node 'require' // The below code is to ensure that the server is run only when not requiring the bundle. diff --git a/src/aai/aai.js b/src/aai/aai.js new file mode 100644 index 00000000000..208a22d3b4d --- /dev/null +++ b/src/aai/aai.js @@ -0,0 +1,154 @@ +'use strict'; +(function(window){ + function AAI() { + var host = 'https://' + window.location.hostname, + ourEntityID = host.match("lindat.mff.cuni.cz") ? "https://ufal-point.mff.cuni.cz" : host; + var namespace = 'repository'; + this.defaults = { + //host : 'https://ufal-point.mff.cuni.cz', + host : host, //better default (useful when testing on ufal-point-dev) + // do not add protocol because an error will appear in the DJ dialog + // if you see the error, your SP is not listed among djc trusted (edugain is enough to be trusted) + responseUrl: window.location.protocol + '//lindat.mff.cuni.cz/idpdiscovery/discojuiceDiscoveryResponse.html', + ourEntityID: ourEntityID + '/shibboleth/eduid/sp', + serviceName: '', + metadataFeed: host + '/xmlui/discojuice/feeds', + selector: 'a.signon', // selector for login button + autoInitialize: true, // auto attach DiscoJuice to DOM + textHelpMore: "First check you are searching under the right country.\nIf your provider is not listed, please read these instructions to obtain an account." + }; + this.setup = function(options) { + var targetUrl = ''; + var opts = jQuery.extend({}, this.defaults, options), + defaultCallback = function(e) { + targetUrl = opts.target + '?redirectUrl='; + // E.g. Redirect to Item page + var redirectUrl = window.location.href; + + // Redirection could be initiated from the login page; in that case, + // we need to retrieve the redirect URL from the URL parameters. + var urlParams = ''; + var redirectUrlFromLogin = ''; + var splitQMarks = window.location.href.split('?'); + if (splitQMarks.length > 1) { + // The redirect URL is in the `1` index of the array in the Shibboleth redirect from the login page + urlParams = new URLSearchParams(splitQMarks[1]); + redirectUrlFromLogin = urlParams.get('redirectUrl') || null; + } + + if (redirectUrlFromLogin != null && redirectUrlFromLogin !== '') { + // Redirect from the login page with retrieved redirect URL + var baseUrl = window.location.origin + formatNamespace(namespace); + var redirectPath = ensureLeadingSlash(redirectUrlFromLogin); + + redirectUrl = baseUrl + redirectPath; + } + + // Encode the redirect URL + targetUrl += window.encodeURIComponent(redirectUrl); + window.location = opts.host + opts.port + '/Shibboleth.sso/Login?SAMLDS=1&target=' + targetUrl + '&entityID=' + window.encodeURIComponent(e.entityID); + }; + //console.log(opts); + if(!opts.target){ + throw 'You need to set the \'target\' parameter.'; + } + // call disco juice setup + if (!opts.autoInitialize || opts.selector.length > 0) { + var djc = DiscoJuice.Hosted.getConfig( + opts.serviceName, + opts.ourEntityID, + opts.responseUrl, + [ ], + opts.host + opts.port + '/Shibboleth.sso/Login?SAMLDS=1&target=' + targetUrl + '&entityID='); + djc.discoPath = window.location.origin + (namespace === '' ? namespace : '/' + namespace) + "/assets/"; + djc.metadata = [opts.metadataFeed]; + djc.subtitle = "Login via Your home institution (e.g. university)"; + djc.textHelp = opts.textHelp; + djc.textHelpMore = opts.textHelpMore; + + djc.inlinemetadata = typeof opts.inlinemetadata === 'object' ? opts.inlinemetadata : []; + djc.inlinemetadata.push({ + 'country': '_all_', + 'entityID': 'https://idm.clarin.eu', + 'geo': {'lat': 51.833298, 'lon': 5.866699}, + 'title': 'Clarin.eu website account', + 'weight': 1000 + }); + djc.inlinemetadata.push({ + 'country': 'CZ', + 'entityID': 'https://cas.cuni.cz/idp/shibboleth', + 'geo': {'lat': '50.0705102', 'lon': '14.4198844'}, + 'title': 'Univerzita Karlova v Praze', + 'weight': -1000 + }); + + if(opts.localauth) { + djc.inlinemetadata.push( + { + 'entityID': 'local://', + 'auth': 'local', + 'title': 'Local authentication', + 'country': '_all_', + 'geo': null, + 'weight': 1000 + }); + djc.callback = function(e){ + var auth = e.auth || null; + switch(auth) { + case 'local': + // DiscoJuice.UI.setScreen(opts.localauth); + // jQuery('input#login').focus(); + // Use cookie to toggle discojuice popup. + setCookie('SHOW_DISCOJUICE_POPUP', false, 1) + window.location = window.location.origin + (namespace === '' ? namespace : '/' + namespace) + "/login?redirectUrl=" + window.location.href; + break; + //case 'saml': + default: + defaultCallback(e); + break; + } + }; + } + + if (opts.callback && typeof opts.callback === 'function') { + djc.callback = function(e) { + opts.callback(e, opts, defaultCallback); + }; + } + + if (opts.autoInitialize) { + jQuery(opts.selector).DiscoJuice( djc ); + } + + return djc; + } //if jQuery(selector) + }; + + // Set a cookie + function setCookie(name, value, daysToExpire) { + var expirationDate = new Date(); + expirationDate.setDate(expirationDate.getDate() + daysToExpire); + + var cookieString = name + '=' + value + ';expires=' + expirationDate.toUTCString() + ';path=/'; + document.cookie = cookieString; + } + + /** + * Return empty string if namespace is empty, otherwise return namespace with leading slash. + */ + function formatNamespace(namespace) { + return namespace === '' ? '' : ensureLeadingSlash(namespace); + } + + /** + * Ensure that the path starts with a leading slash. + */ + function ensureLeadingSlash(path) { + return path.startsWith('/') ? path : '/' + path; + } + } + + if (!window.aai) { + window.aai = new AAI(); + } +})(window); diff --git a/src/aai/aai_config.js b/src/aai/aai_config.js new file mode 100644 index 00000000000..ce5480f9367 --- /dev/null +++ b/src/aai/aai_config.js @@ -0,0 +1,45 @@ +/*global jQuery */ +/*jshint globalstrict: true*/ +'use strict'; + +jQuery(document).ready( + function () { + var opts = (function () { + var instance = {}; + //if ever port is needed (eg. testing other tomcat) it should be in responseUrl and target + instance.port = (window.location.port === "" ? "" : ":" + window.location.port); + instance.host = window.location.protocol + '//' + + window.location.hostname; + instance.repoPath = jQuery("a#repository_path").attr("href"); + if (instance.repoPath.charAt(instance.repoPath.length - 1) !== '/') { + instance.repoPath = instance.repoPath + '/'; + } + instance.target = instance.repoPath; + + //In order to use the discojuice store (improve score of used IDPs) + //Works only with "verified" SPs - ie. ufal-point, displays error on ufal-point-dev + instance.responseUrl = + (window.location.hostname.search("ufal-point-dev") >= 0) ? + "" : + instance.host + instance.port + instance.repoPath + + "themes/UFAL/lib/html/disco-juice.html?"; + // e.g., instance.metadataFeed = "http://localhost:8080/server/api/discojuice/feeds?callback=dj_md_1"; + instance.metadataFeed = instance.target + "discojuice/feeds"; + instance.serviceName = "LINDAT/CLARIAH-CZ Repository"; + instance.localauth = + '
' + + '

Sign in using your local account obtained from the LINDAT/CLARIAH-CZ administrator.

' + + '

' + + '

' + + '

Forgot your password?

' + + '

' + + '
'; + instance.target = instance.target + "authn/shibboleth"; + return instance; + })(); + if (!("aai" in window)) { + throw "Failed to find UFAL AAI object. See https://redmine.ms.mff.cuni.cz/projects/lindat-aai for more details!"; + } + window.aai.setup(opts); + } +); // ready diff --git a/src/aai/discojuice/discojuice-2.1.en.min.js b/src/aai/discojuice/discojuice-2.1.en.min.js new file mode 100644 index 00000000000..55c5a9d624b --- /dev/null +++ b/src/aai/discojuice/discojuice-2.1.en.min.js @@ -0,0 +1 @@ +discojuice.js \ No newline at end of file diff --git a/src/aai/discojuice/discojuice.css b/src/aai/discojuice/discojuice.css new file mode 100644 index 00000000000..c217f1f5982 --- /dev/null +++ b/src/aai/discojuice/discojuice.css @@ -0,0 +1,447 @@ + + +/* + * Generic css for whole popup box + */ +div.discojuice { + font-family: Arial; + +/* font-size: small;*/ + z-index: 100; + margin: 0; + padding: 0; + width: 500px; + position: absolute; + top: 30px; + right: 10px; + z-index: 150; + +} + +/*div.discojuice * { + color: #000; + background: none; +}*/ + +div.discojuice p { + margin: 2px; padding: 0px; +} + +div.discojuice form.discojuice_up { + padding: 0px; + margin: 0px; + font-family: Helvetica; +} +/*div.discojuice form.discojuice_up h2 {*/ +/* margin: 0px inherit 3px inherit;*/ +/*}*/ +div.discojuice form.discojuice_up p{ + padding: 0px; margin: 0px; +} +div.discojuice form.discojuice_up label.discojuice_up { + display: block; + margin: 22px 5px 0px 0px; + font-size: 160%; + color: #444; + +} +div.discojuice form.discojuice_up input.discojuice_up { + width: 60%; + font-size: 200%; + border-radius: 6px; + border: 1px solid #aaa; + padding: 6px 20px; + background: #fff; + margin: 0px 5px 3px 0px; +} +div.discojuice form.discojuice_up input.submit { + font-size: 105px ! important; +} + + +div.discojuice div.discojuice_page { + +} + +div.discojuice p#dj_help { + cursor: pointer; +} + + + +div.discojuice > div.top { + + background: #fff; + border-bottom: 1px solid #bbb; + + -webkit-border-top-left-radius: 15px; + -webkit-border-top-right-radius: 15px; + -moz-border-radius-topleft: 15px; + -moz-border-radius-topright: 15px; + border-top-left-radius: 15px; + border-top-right-radius: 15px; +} + +div.discojuice > div { + + background: #eee; + border-bottom: 1px solid #bbb; + + padding: 8px 14px; + margin: 0; +} + +div.discojuice > div.bottom { +/* background: url(./images/box-bottom.png) no-repeat 0% 100%;*/ + + background: #f8f8f8; + + padding: 10px 17px; + margin: 0; + + -webkit-border-bottom-right-radius: 15px; + -webkit-border-bottom-left-radius: 15px; + -moz-border-radius-bottomright: 15px; + -moz-border-radius-bottomleft: 15px; + border-bottom-right-radius: 15px; + border-bottom-left-radius: 15px; + +} + +div.discojuice .discojuice_maintitle { + font-size: 15px; + font-family: Tahoma, Helvetica; + font-weight: normal; + color: #666; +} + +div.discojuice .discojuice_subtitle { + font-size: 12px; + font-family: Tahoma, Helvetica; + font-weight: normal; + color: #888; +} + +div.discojuice .discojuice_close { + width: 62px; + height: 29px; + background: url(./images/close.png) no-repeat; + text-decoration: none; + float: right; +} + +div.discojuice .discojuice_close:hover { + background: url(./images/close-hover.png) no-repeat; +} + + +div.discojuice a { + outline: none; + color: #444; + text-decoration: none; +} + +div.discojuice a img { + border: none; + outline: none; +} + +div.discojuice a.textlink:hover { + color: #666; + border-bottom: 1px solid #aaa; +} + + + + + + +/* + * Section for the scroller + */ +div.discojuice .discojuice_listContent { + overflow: auto; +/* max-height: 40%; */ + max-height: 450px; +} +div.discojuice div.scroller { + padding: 1px 1px 10px 1px; +} +div.discojuice div.scroller img.logo { + margin: 0px; + float: right; +} + +div.discojuice div.scroller a { + padding: 3px 6px; + font-size: 100% ! important; +} +div.discojuice div.scroller a span { +/* margin: 3px;*/ +/* display: block;*/ +} +div.discojuice div.scroller a span.title { + margin-right: .4em; +} +div.discojuice div.scroller a span.substring { + font-size: 95%; + color: #777; +} +div.discojuice div.scroller a span.distance { + font-size: 90%; + color: #aaa; +} + +div.discojuice div.scroller a span.location { + display: block; +} +div.discojuice div.scroller a span.country { + font-size: 86%; + color: #555; + margin-right: 7px; +} +div.discojuice div.scroller a div.debug { + font-size: 86%; + color: #aaa; +} + + +div.discojuice div.scroller hr { + margin: 0px; + padding: 0px; +} + + +div.discojuice div.scroller.filtered a { + display: none !important; +} + +div.discojuice div.scroller.filtered a.present { + display: inline-block !important; +} + + +div.discojuice div.loadingData { + color: #aaa; +} + + + +/* + * Section for the filters + */ + + + + + + + + +/* + * Section for the search box + */ +div.discojuice input.discojuice_search { + width: 100%; +} + + + + + + + + + + + + + +/* + * ------ SECTION FOR THE IDP Buttons ----- + */ + +/* Generals */ +div.discojuice div.scroller a { + margin: 4px 2px 0px 0px; + display: block; + + border: 1px solid #bbb; + border-radius: 4px; + -moz-border-radius:4px; + -webkit-border-radius:4px; + + background-color: #fafafa; + + /*background-image: -webkit-gradient(*/ + /* linear,*/ + /* left bottom,*/ + /* left top,*/ + /* color-stop(0.3, rgb(220,220,220)),*/ + /* color-stop(0.9, rgb(240,240,240))*/ + /*);*/ + /*background-image: -moz-linear-gradient(*/ + /* bottom,*/ + /* rgb(220,220,220) 30%,*/ + /* rgb(240,240,240) 90%*/ + /*);*/ + background-image: -webkit-gradient( + linear, + left bottom, + left top, + color-stop(0.3, rgb(220,220,220)), + color-stop(0.9, rgb(240,240,240)) ); + + /* Text */ + color: #333; + text-shadow: 0 1px #fff; + font-size: 135%; + font-family: "Arial Narrow", "Arial", sans-serif; + text-decoration: none; +} + +/* Shaddow effect for normal entries... */ +div.discojuice div.scroller a { +/* box-shadow: inset 0 1px 3px #fff, inset 0 -15px #cbe6f2, 0 0 3px #8ec1da;*/ +} + + +/* Item that is hovered. */ +div.discojuice div.scroller a:hover, div.discojuice div.scroller a.hothit:hover { + background-color: #fafafa; + border: 1px solid #666! important; +} +div.discojuice div.scroller a:hover { + background-color: #fafafa; + border: 1px solid #666; + +/* + -o-box-shadow: none; + -webkit-box-shadow:none; + -moz-box-shadow: none; + color: #333; + text-shadow: 0 1px #fff; +*/ +} + + +/* Highlight the entry that is listed on top reccomended. + * usually because the user has selected that item before. + */ +div.discojuice div.scroller a.hothit { +/* border: 3px solid #ccc;*/ + border: 1px solid #aaa; +/* background-color: #daebf3;*/ + color: #333; + margin-bottom: 14px; + + border-radius: 4px; + box-shadow: 0 0 5px #ccc; + -o-box-shadow: 0 0 5px #ccc; + -webkit-box-shadow: 0 0 5px #ccc; + -moz-box-shadow: 0 0 5px #ccc; + color: #333; + text-shadow: 0 1px #fff; +} + +div.discojuice div.scroller a.disabled span.title { + color: #999 !important; +} +div.discojuice div.scroller a.disabled span.location { + color: #999 !important; +} + + + + +/* + * ------ END OF ---- SECTION FOR THE IDP Buttons ----- + */ + + + + + + + + + + + + + + + + +div.discojuice a#moreoptions, a.discojuice_what { + font-weight: bold; + padding-left: 12px; + background: url(./images/arrow.png) no-repeat 0px 3px; +} + +div.discojuice .discojuice_whatisthis.show a.discojuice_what { + background: url(./images/arrow-r.png) no-repeat 0px 5px; +} + +div.discojuice p.moretext { + margin-top: 0; + color: #777; +} + +div.discojuice div.discojuice_whatisthis { + margin-bottom: 10px; +} + +div.discojuice .discojuice_whattext { + display: none; + margin-top: 1px; + margin-left: 12px; + margin-bottom: 0; + padding: 0; + font-size: 11px; + color: #555; +} + +div.discojuice .discojuice_whatisthis.show .discojuice_whattext { + display: block; +} + + + + +/* + * Overlay grey out background + */ + +div#discojuice_overlay { + background-color: black; + filter:alpha(opacity=50); /* IE */ + opacity: 0.5; /* Safari, Opera */ + -moz-opacity:0.50; /* FireFox */ + z-index: 20; + height: 100%; + width: 100%; + background-repeat:no-repeat; + background-position:center; + position:absolute; + top: 0px; + left: 0px; +} + + + +@media (max-width: 979px){ +.discojuice { + width: auto !important; + max-width: 380px; + margin-left: 10px !important; +} + +.discojuice_listContent { + max-height: 200px !important; +} + +#discojuice_overlay { + position: fixed !important; +} +} diff --git a/src/aai/discojuice/discojuice.js b/src/aai/discojuice/discojuice.js new file mode 100644 index 00000000000..4fd1397fcb4 --- /dev/null +++ b/src/aai/discojuice/discojuice.js @@ -0,0 +1,95 @@ +(function(a){function c(c){function h(){c?l.removeData(c):o&&delete d[o]}function f(){j.id=setTimeout(function(){j.fn()},q)}var k=this,l,j={},m=c?a.fn:a,n=arguments,r=4,o=n[1],q=n[2],p=n[3];"string"!==typeof o&&(r--,o=c=0,q=n[1],p=n[2]);c?(l=k.eq(0),l.data(c,j=l.data(c)||{})):o&&(j=d[o]||(d[o]={}));j.id&&clearTimeout(j.id);delete j.id;if(p)j.fn=function(a){"string"===typeof p&&(p=m[p]);!0===p.apply(k,e.call(n,r))&&!a?f():h()},f();else{if(j.fn)return void 0===q?h():j.fn(!1===q),!0;h()}}var d={},e= +Array.prototype.slice;a.doTimeout=function(){return c.apply(window,[0].concat(e.call(arguments)))};a.fn.doTimeout=function(){var a=e.call(arguments),d=c.apply(this,["doTimeout"+a[0]].concat(a));return"number"===typeof a[0]||"number"===typeof a[1]?this:d}})(jQuery);if("undefined"==typeof console)var console={log:function(){}}; +var DiscoJuice={Constants:{Countries:{AF:"Afghanistan",AX:"\u00c5land Islands",AL:"Albania",DZ:"Algeria",AS:"American Samoa",AD:"Andorra",AO:"Angola",AI:"Anguilla",AQ:"Antarctica",AG:"Antigua and Barbuda",AR:"Argentina",AM:"Armenia",AW:"Aruba",AC:"Ascension Island",AU:"Australia",AT:"Austria",AZ:"Azerbaijan",BS:"Bahamas",BH:"Bahrain",BD:"Bangladesh",BB:"Barbados",BY:"Belarus",BE:"Belgium",BZ:"Belize",BJ:"Benin",BM:"Bermuda",BT:"Bhutan",BO:"Bolivia",BQ:"Bonaire, Sint Eustatius and Saba",BA:"Bosnia and Herzegovina", +BW:"Botswana",BV:"Bouvet Island",BR:"Brazil",IO:"British Indian Ocean Territory",VG:"British Virgin Islands",BN:"Brunei Darussalam",BG:"Bulgaria",BF:"Burkina Faso",MM:"Burma",BI:"Burundi",KH:"Cambodia",CM:"Cameroon",CA:"Canada",CV:"Cape Verde",KY:"Cayman Islands",CF:"Central African Republic",TD:"Chad",CL:"Chile",CN:"China",CX:"Christmas Island",CC:"Cocos (Keeling) Islands",CO:"Colombia",KM:"Comoros",CD:"Congo, Democratic Republic of the",CG:"Congo, Republic of the",CK:"Cook Islands",CR:"Costa Rica", +CI:"C\u00f4te d'Ivoire",HR:"Croatia",CU:"Cuba",CW:"Cura\u00e7ao",CY:"Cyprus",CZ:"Czech Republic",DK:"Denmark",DJ:"Djibouti",DM:"Dominica",DO:"Dominican Republic",EC:"Ecuador",EG:"Egypt",SV:"El Salvador",GQ:"Equatorial Guinea",ER:"Eritrea",EE:"Estonia",ET:"Ethiopia",FK:"Falkland Islands",FO:"Faroe Islands",FJ:"Fiji",FI:"Finland",FR:"France",GF:"French Guiana",PF:"French Polynesia",TF:"French Southern and Antarctic Lands",GA:"Gabon",GM:"Gambia",GE:"Georgia",DE:"Germany",GH:"Ghana",GI:"Gibraltar",GR:"Greece", +GL:"Greenland",GD:"Grenada",GP:"Guadeloupe",GU:"Guam",GT:"Guatemala",GG:"Guernsey",GN:"Guinea",GW:"Guinea-Bissau",GY:"Guyana",HT:"Haiti",HM:"Heard Island and McDonald Islands",HN:"Honduras",HK:"Hong Kong",HU:"Hungary",IS:"Iceland",IN:"India",ID:"Indonesia",IR:"Iran",IQ:"Iraq",IE:"Ireland",IM:"Isle of Man",IL:"Israel",IT:"Italy",JM:"Jamaica",JP:"Japan",JE:"Jersey",JO:"Jordan",KZ:"Kazakhstan",KE:"Kenya",KI:"Kiribati",KP:"North Korea",KR:"South Korea",KW:"Kuwait",KG:"Kyrgyzstan",LA:"Laos",LV:"Latvia", +LB:"Lebanon",LS:"Lesotho",LR:"Liberia",LY:"Libya",LI:"Liechtenstein",LT:"Lithuania",LU:"Luxembourg",MO:"Macau",MK:"Macedonia",MG:"Madagascar",MW:"Malawi",MY:"Malaysia",MV:"Maldives",ML:"Mali",MT:"Malta",MH:"Marshall Islands",MQ:"Martinique",MR:"Mauritania",MU:"Mauritius",YT:"Mayotte",MX:"Mexico",FM:"Micronesia, Federated States of",MD:"Moldova",MC:"Monaco",MN:"Mongolia",ME:"Montenegro",MS:"Montserrat",MA:"Morocco",MZ:"Mozambique",NA:"Namibia",NR:"Nauru",NP:"Nepal",NL:"Netherlands",NC:"New Caledonia", +NZ:"New Zealand",NI:"Nicaragua",NE:"Niger",NG:"Nigeria",NU:"Niue",NF:"Norfolk Island",MP:"Northern Mariana Islands",NO:"Norway",OM:"Oman",PK:"Pakistan",PW:"Palau",PS:"Palestine",PA:"Panama",PG:"Papua New Guinea",PY:"Paraguay",PE:"Peru",PH:"Philippines",PN:"Pitcairn Islands",PL:"Poland",PT:"Portugal",PR:"Puerto Rico",QA:"Qatar",RE:"R\u00e9union",RO:"Romania",RU:"Russia",RW:"Rwanda",BL:"Saint Barth\u00e9lemy",SH:"Saint Helena, Ascension and Tristan da Cunha",KN:"Saint Kitts and Nevis",LC:"Saint Lucia", +MF:"Saint Martin",PM:"Saint Pierre and Miquelon",VC:"Saint Vincent and the Grenadines",WS:"Samoa",SM:"San Marino",ST:"S\u00e3o Tom\u00e9 and Pr\u00edncipe",SA:"Saudi Arabia",SN:"Senegal",RS:"Serbia",SC:"Seychelles",SL:"Sierra Leone",SG:"Singapore",SX:"Sint Maarten",SK:"Slovakia",SI:"Slovenia",SB:"Solomon Islands",SO:"Somalia",ZA:"South Africa",GS:"South Georgia and the South Sandwich Islands",ES:"Spain",LK:"Sri Lanka",SD:"Sudan",SR:"Suriname",SJ:"Svalbard and Jan Mayen",SZ:"Swaziland",SE:"Sweden", +CH:"Switzerland",SY:"Syria",TW:"Taiwan",TJ:"Tajikistan",TZ:"Tanzania",TH:"Thailand",TL:"Timor-Leste",TG:"Togo",TK:"Tokelau",TO:"Tonga",TT:"Trinidad and Tobago",TN:"Tunisia",TR:"Turkey",TM:"Turkmenistan",TC:"Turks and Caicos Islands",TV:"Tuvalu",UG:"Uganda",UA:"Ukraine",GB:"UK",AE:"United Arab Emirates",UM:"United States Minor Outlying Islands",UY:"Uruguay",US:"USA",UZ:"Uzbekistan",VU:"Vanuatu",VA:"Vatican City",VE:"Venezuela",VN:"Viet Nam",VI:"Virgin Islands, U.S.",WF:"Wallis and Futuna",EH:"Western Sahara", +YE:"Yemen",ZM:"Zambia",ZW:"Zimbabwe",XX:"Experimental"},Flags:{AD:"ad.png",AE:"ae.png",AF:"af.png",AG:"ag.png",AI:"ai.png",AL:"al.png",AM:"am.png",AN:"an.png",AO:"ao.png",AR:"ar.png",AS:"as.png",AT:"at.png",AU:"au.png",AW:"aw.png",AX:"ax.png",AZ:"az.png",BA:"ba.png",BB:"bb.png",BD:"bd.png",BE:"be.png",BF:"bf.png",BG:"bg.png",BH:"bh.png",BI:"bi.png",BJ:"bj.png",BM:"bm.png",BN:"bn.png",BO:"bo.png",BR:"br.png",BS:"bs.png",BT:"bt.png",BV:"bv.png",BW:"bw.png",BY:"by.png",BZ:"bz.png",CA:"ca.png",CC:"cc.png", +CD:"cd.png",CF:"cf.png",CG:"cg.png",CH:"ch.png",CI:"ci.png",CK:"ck.png",CL:"cl.png",CM:"cm.png",CN:"cn.png",CO:"co.png",CR:"cr.png",CS:"cs.png",CU:"cu.png",CV:"cv.png",CX:"cx.png",CY:"cy.png",CZ:"cz.png",DE:"de.png",DJ:"dj.png",DK:"dk.png",DM:"dm.png",DO:"do.png",DZ:"dz.png",EC:"ec.png",EE:"ee.png",EG:"eg.png",EH:"eh.png",ER:"er.png",ES:"es.png",ET:"et.png",FI:"fi.png",FJ:"fj.png",FK:"fk.png",FM:"fm.png",FO:"fo.png",FR:"fr.png",GA:"ga.png",GB:"gb.png",GD:"gd.png",GE:"ge.png",GF:"gf.png",GH:"gh.png", +GI:"gi.png",GL:"gl.png",GM:"gm.png",GN:"gn.png",GP:"gp.png",GQ:"gq.png",GR:"gr.png",GS:"gs.png",GT:"gt.png",GU:"gu.png",GW:"gw.png",GY:"gy.png",HK:"hk.png",HM:"hm.png",HN:"hn.png",HR:"hr.png",HT:"ht.png",HU:"hu.png",ID:"id.png",IE:"ie.png",IL:"il.png",IN:"in.png",IO:"io.png",IQ:"iq.png",IR:"ir.png",IS:"is.png",IT:"it.png",JM:"jm.png",JO:"jo.png",JP:"jp.png",KE:"ke.png",KG:"kg.png",KH:"kh.png",KI:"ki.png",KM:"km.png",KN:"kn.png",KP:"kp.png",KR:"kr.png",KW:"kw.png",KY:"ky.png",KZ:"kz.png",LA:"la.png", +LB:"lb.png",LC:"lc.png",LI:"li.png",LK:"lk.png",LR:"lr.png",LS:"ls.png",LT:"lt.png",LU:"lu.png",LV:"lv.png",LY:"ly.png",MA:"ma.png",MC:"mc.png",MD:"md.png",ME:"me.png",MG:"mg.png",MH:"mh.png",MK:"mk.png",ML:"ml.png",MM:"mm.png",MN:"mn.png",MO:"mo.png",MP:"mp.png",MQ:"mq.png",MR:"mr.png",MS:"ms.png",MT:"mt.png",MU:"mu.png",MV:"mv.png",MW:"mw.png",MX:"mx.png",MY:"my.png",MZ:"mz.png",NA:"na.png",NC:"nc.png",NE:"ne.png",NF:"nf.png",NG:"ng.png",NI:"ni.png",NL:"nl.png",NO:"no.png",NP:"np.png",NR:"nr.png", +NU:"nu.png",NZ:"nz.png",OM:"om.png",PA:"pa.png",PE:"pe.png",PF:"pf.png",PG:"pg.png",PH:"ph.png",PK:"pk.png",PL:"pl.png",PM:"pm.png",PN:"pn.png",PR:"pr.png",PS:"ps.png",PT:"pt.png",PW:"pw.png",PY:"py.png",QA:"qa.png",RE:"re.png",RO:"ro.png",RS:"rs.png",RU:"ru.png",RW:"rw.png",SA:"sa.png",SB:"sb.png",SC:"sc.png",SD:"sd.png",SE:"se.png",SG:"sg.png",SH:"sh.png",SI:"si.png",SJ:"sj.png",SK:"sk.png",SL:"sl.png",SM:"sm.png",SN:"sn.png",SO:"so.png",SR:"sr.png",ST:"st.png",SV:"sv.png",SY:"sy.png",SZ:"sz.png", +TC:"tc.png",TD:"td.png",TF:"tf.png",TG:"tg.png",TH:"th.png",TJ:"tj.png",TK:"tk.png",TL:"tl.png",TM:"tm.png",TN:"tn.png",TO:"to.png",TR:"tr.png",TT:"tt.png",TV:"tv.png",TW:"tw.png",TZ:"tz.png",UA:"ua.png",UG:"ug.png",UM:"um.png",US:"us.png",UY:"uy.png",UZ:"uz.png",VA:"va.png",VC:"vc.png",VE:"ve.png",VG:"vg.png",VI:"vi.png",VN:"vn.png",VU:"vu.png",WF:"wf.png",WS:"ws.png",YE:"ye.png",YT:"yt.png",ZA:"za.png",ZM:"zm.png",ZW:"zw.png"}}}; +DiscoJuice.Utils={log:function(a){console.log(a)},options:function(){var a;return{get:function(c,d){return!a||"undefined"===typeof a[c]?d:a[c]},set:function(c){a=c},update:function(c,d){a[c]=d}}}(),escapeHTML:function(a){return a.replace(/&/g,"&").replace(/>/g,">").replace(/arguments.length)&&RegExp){for(var a= +arguments[0],c=/([^%]*)%('.|0|\x20)?(-)?(\d+)?(\.\d+)?(%|b|c|d|u|f|o|s|x|X)(.*)/,d=b=[],e=0,g=0;d=c.exec(a);){var a=d[1],h=d[2],f=d[4],k=d[5],l=d[6],d=d[7];g++;if("%"==l)k="%";else{e++;e>=arguments.length&&alert("Error! Not enough function arguments ("+(arguments.length-1)+", excluding the string)\nfor the number of substitution parameters in string ("+e+" so far).");var j=arguments[e],m="";h&&"'"==h.substr(0,1)?m=a.substr(1,1):h&&(m=h);h=-1;f&&(h=parseInt(f));f=-1;k&&"f"==l&&(f=parseInt(k.substring(1))); +k=j;switch(l){case "b":k=parseInt(j).toString(2);break;case "c":k=String.fromCharCode(parseInt(j));break;case "d":k=parseInt(j)?parseInt(j):0;break;case "u":k=Math.abs(j);break;case "f":k=-1