mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch '4scienceGithub/DURACOM-235' into DURACOM-235
# Conflicts: # src/app/collection-page/create-collection-page/create-collection-page.component.html # src/app/shared/comcol/comcol-forms/create-comcol-page/create-comcol-page.component.ts
This commit is contained in:
@@ -8,7 +8,16 @@
|
||||
"eslint-plugin-deprecation",
|
||||
"unused-imports",
|
||||
"eslint-plugin-lodash",
|
||||
"eslint-plugin-jsonc"
|
||||
"eslint-plugin-jsonc",
|
||||
"eslint-plugin-rxjs",
|
||||
"eslint-plugin-simple-import-sort",
|
||||
"eslint-plugin-import-newlines",
|
||||
"eslint-plugin-jsonc",
|
||||
"dspace-angular-ts",
|
||||
"dspace-angular-html"
|
||||
],
|
||||
"ignorePatterns": [
|
||||
"lint/test/fixture"
|
||||
],
|
||||
"overrides": [
|
||||
{
|
||||
@@ -18,7 +27,8 @@
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"./tsconfig.json",
|
||||
"./cypress/tsconfig.json"
|
||||
"./cypress/tsconfig.json",
|
||||
"./lint/tsconfig.json"
|
||||
],
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
@@ -27,17 +37,32 @@
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"plugin:@angular-eslint/recommended",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
"plugin:@angular-eslint/template/process-inline-templates",
|
||||
"plugin:rxjs/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"indent": [
|
||||
"error",
|
||||
2,
|
||||
{
|
||||
"SwitchCase": 1,
|
||||
"ignoredNodes": [
|
||||
"ClassBody.body > PropertyDefinition[decorators.length > 0] > .key"
|
||||
]
|
||||
}
|
||||
],
|
||||
"max-classes-per-file": [
|
||||
"error",
|
||||
1
|
||||
],
|
||||
"comma-dangle": [
|
||||
"off",
|
||||
"error",
|
||||
"always-multiline"
|
||||
],
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"eol-last": [
|
||||
"error",
|
||||
"always"
|
||||
@@ -104,15 +129,13 @@
|
||||
"allowTernary": true
|
||||
}
|
||||
],
|
||||
"prefer-const": "off", // todo: re-enable & fix errors (more strict than it used to be in TSLint)
|
||||
"prefer-const": "error",
|
||||
"no-case-declarations": "error",
|
||||
"no-extra-boolean-cast": "error",
|
||||
"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",
|
||||
@@ -139,7 +162,6 @@
|
||||
}
|
||||
],
|
||||
"@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",
|
||||
@@ -183,7 +205,7 @@
|
||||
],
|
||||
"@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/ban-types": "error",
|
||||
"@typescript-eslint/no-floating-promises": "warn",
|
||||
"@typescript-eslint/no-misused-promises": "warn",
|
||||
"@typescript-eslint/restrict-plus-operands": "warn",
|
||||
@@ -200,17 +222,65 @@
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/no-base-to-string": [
|
||||
"error",
|
||||
{
|
||||
"ignoredTypeNames": [
|
||||
"ResourceType",
|
||||
"Error"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
"deprecation/deprecation": "warn",
|
||||
|
||||
"simple-import-sort/imports": "error",
|
||||
"simple-import-sort/exports": "error",
|
||||
"import/order": "off",
|
||||
"import/first": "error",
|
||||
"import/newline-after-import": "error",
|
||||
"import/no-duplicates": "error",
|
||||
"import/no-deprecated": "warn",
|
||||
"import/no-namespace": "error",
|
||||
"import-newlines/enforce": [
|
||||
"error",
|
||||
{
|
||||
"items": 1,
|
||||
"semi": true,
|
||||
"forceSingleLine": true
|
||||
}
|
||||
],
|
||||
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"lodash/import-scope": [
|
||||
"error",
|
||||
"method"
|
||||
]
|
||||
],
|
||||
|
||||
"rxjs/no-nested-subscribe": "off", // todo: go over _all_ cases
|
||||
|
||||
// Custom DSpace Angular rules
|
||||
"dspace-angular-ts/themed-component-classes": "error",
|
||||
"dspace-angular-ts/themed-component-selectors": "error",
|
||||
"dspace-angular-ts/themed-component-usages": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"*.spec.ts"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"./tsconfig.json",
|
||||
"./cypress/tsconfig.json"
|
||||
],
|
||||
"createDefaultProgram": true
|
||||
},
|
||||
"rules": {
|
||||
"prefer-const": "off",
|
||||
|
||||
// Custom DSpace Angular rules
|
||||
"dspace-angular-ts/themed-component-usages": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -221,9 +291,8 @@
|
||||
"plugin:@angular-eslint/template/recommended"
|
||||
],
|
||||
"rules": {
|
||||
// todo: re-enable & fix errors
|
||||
"@angular-eslint/template/no-negated-async": "off",
|
||||
"@angular-eslint/template/eqeqeq": "off"
|
||||
// Custom DSpace Angular rules
|
||||
"dspace-angular-html/themed-component-usages": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
5
.gitattributes
vendored
5
.gitattributes
vendored
@@ -13,4 +13,7 @@
|
||||
*.css eol=lf
|
||||
*.scss eol=lf
|
||||
*.html eol=lf
|
||||
*.svg eol=lf
|
||||
*.svg eol=lf
|
||||
|
||||
# Generated documentation should have LF line endings to reduce git noise
|
||||
docs/lint/**/*.md eol=lf
|
39
.github/workflows/build.yml
vendored
39
.github/workflows/build.yml
vendored
@@ -33,12 +33,12 @@ jobs:
|
||||
#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
|
||||
# Project name to use when running "docker compose" prior to e2e tests
|
||||
COMPOSE_PROJECT_NAME: 'ci'
|
||||
strategy:
|
||||
# Create a matrix of Node versions to test against (in parallel)
|
||||
matrix:
|
||||
node-version: [16.x, 18.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
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
- name: Cache Yarn dependencies
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
# Cache entire Yarn cache directory (see previous step)
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
@@ -85,8 +85,14 @@ jobs:
|
||||
- name: Install Yarn dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
- name: Build lint plugins
|
||||
run: yarn run build:lint
|
||||
|
||||
- name: Run lint plugin tests
|
||||
run: yarn run test:lint:nobuild
|
||||
|
||||
- name: Run lint
|
||||
run: yarn run lint --quiet
|
||||
run: yarn run lint:nobuild --quiet
|
||||
|
||||
- name: Check for circular dependencies
|
||||
run: yarn run check-circ-deps
|
||||
@@ -101,19 +107,19 @@ jobs:
|
||||
# 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
|
||||
- name: Upload code coverage report to Artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.node-version == '18.x'
|
||||
with:
|
||||
name: dspace-angular coverage report
|
||||
name: coverage-report-${{ matrix.node-version }}
|
||||
path: 'coverage/dspace-angular/lcov.info'
|
||||
retention-days: 14
|
||||
|
||||
# Using docker-compose start backend using CI configuration
|
||||
# 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
|
||||
@@ -135,19 +141,19 @@ jobs:
|
||||
# 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@v3
|
||||
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@v3
|
||||
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)
|
||||
@@ -182,7 +188,7 @@ jobs:
|
||||
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.
|
||||
@@ -197,7 +203,7 @@ jobs:
|
||||
|
||||
# Download artifacts from previous 'tests' job
|
||||
- name: Download coverage artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
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.
|
||||
@@ -207,11 +213,12 @@ jobs:
|
||||
- name: Upload coverage to Codecov.io
|
||||
uses: Wandalen/wretry.action@v1.3.0
|
||||
with:
|
||||
action: codecov/codecov-action@v3
|
||||
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
|
||||
|
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -28,7 +28,7 @@ jobs:
|
||||
# Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image
|
||||
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main
|
||||
with:
|
||||
build_id: dspace-angular
|
||||
build_id: dspace-angular-dev
|
||||
image_name: dspace/dspace-angular
|
||||
dockerfile_path: ./Dockerfile
|
||||
secrets:
|
||||
|
2
.github/workflows/issue_opened.yml
vendored
2
.github/workflows/issue_opened.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
# 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: actions/add-to-project@v0.5.0
|
||||
uses: actions/add-to-project@v1.0.0
|
||||
# Note, the authentication token below is an ORG level Secret.
|
||||
# It must be created/recreated manually via a personal access token with admin:org, project, public_repo permissions
|
||||
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token
|
||||
|
2
.github/workflows/pull_request_opened.yml
vendored
2
.github/workflows/pull_request_opened.yml
vendored
@@ -21,4 +21,4 @@ jobs:
|
||||
# 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.0.1
|
||||
uses: toshimaru/auto-author-assign@v2.1.0
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/.angular/cache
|
||||
/.nx
|
||||
/__build__
|
||||
/__server_build__
|
||||
/node_modules
|
||||
|
20
angular.json
20
angular.json
@@ -109,22 +109,22 @@
|
||||
"serve": {
|
||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "dspace-angular:build",
|
||||
"buildTarget": "dspace-angular:build",
|
||||
"port": 4000
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"browserTarget": "dspace-angular:build:development"
|
||||
"buildTarget": "dspace-angular:build:development"
|
||||
},
|
||||
"production": {
|
||||
"browserTarget": "dspace-angular:build:production"
|
||||
"buildTarget": "dspace-angular:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "dspace-angular:build"
|
||||
"buildTarget": "dspace-angular:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
@@ -217,23 +217,23 @@
|
||||
}
|
||||
},
|
||||
"serve-ssr": {
|
||||
"builder": "@nguniversal/builders:ssr-dev-server",
|
||||
"builder": "@angular-devkit/build-angular:ssr-dev-server",
|
||||
"options": {
|
||||
"browserTarget": "dspace-angular:build",
|
||||
"buildTarget": "dspace-angular:build",
|
||||
"serverTarget": "dspace-angular:server",
|
||||
"port": 4000
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "dspace-angular:build:production",
|
||||
"buildTarget": "dspace-angular:build:production",
|
||||
"serverTarget": "dspace-angular:server:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"prerender": {
|
||||
"builder": "@nguniversal/builders:prerender",
|
||||
"builder": "@angular-devkit/build-angular:prerender",
|
||||
"options": {
|
||||
"browserTarget": "dspace-angular:build:production",
|
||||
"buildTarget": "dspace-angular:build:production",
|
||||
"serverTarget": "dspace-angular:server:production",
|
||||
"routes": [
|
||||
"/"
|
||||
@@ -266,6 +266,8 @@
|
||||
"options": {
|
||||
"lintFilePatterns": [
|
||||
"src/**/*.ts",
|
||||
"cypress/**/*.ts",
|
||||
"lint/**/*.ts",
|
||||
"src/**/*.html",
|
||||
"src/**/*.json5"
|
||||
]
|
||||
|
@@ -17,6 +17,13 @@ ui:
|
||||
# Trust X-FORWARDED-* headers from proxies (default = true)
|
||||
useProxies: true
|
||||
|
||||
universal:
|
||||
# Whether to inline "critical" styles into the server-side rendered HTML.
|
||||
# Determining which styles are critical is a relatively expensive operation;
|
||||
# this option can be disabled to boost server performance at the expense of
|
||||
# loading smoothness.
|
||||
inlineCriticalCss: true
|
||||
|
||||
# The REST API server settings
|
||||
# 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.
|
||||
@@ -131,12 +138,16 @@ submission:
|
||||
# NOTE: after how many time (milliseconds) submission is saved automatically
|
||||
# eg. timer: 5 * (1000 * 60); // 5 minutes
|
||||
timer: 0
|
||||
# Always show the duplicate detection section if enabled, even if there are no potential duplicates detected
|
||||
# (a message will be displayed to indicate no matches were found)
|
||||
duplicateDetection:
|
||||
alwaysShowSection: false
|
||||
icons:
|
||||
metadata:
|
||||
# NOTE: example of configuration
|
||||
# # NOTE: metadata name
|
||||
# - name: dc.author
|
||||
# # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
|
||||
# # NOTE: fontawesome (v6.x) icon classes and bootstrap utility classes can be used
|
||||
# style: fas fa-user
|
||||
- name: dc.author
|
||||
style: fas fa-user
|
||||
@@ -147,18 +158,40 @@ submission:
|
||||
confidence:
|
||||
# NOTE: example of configuration
|
||||
# # NOTE: confidence value
|
||||
# - name: dc.author
|
||||
# # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
|
||||
# style: fa-user
|
||||
# - value: 600
|
||||
# # NOTE: fontawesome (v6.x) icon classes and bootstrap utility classes can be used
|
||||
# style: text-success
|
||||
# icon: fa-circle-check
|
||||
# # NOTE: the class configured in property style is used by default, the icon property could be used in component
|
||||
# configured to use a 'icon mode' display (mainly in edit-item page)
|
||||
- value: 600
|
||||
style: text-success
|
||||
icon: fa-circle-check
|
||||
- value: 500
|
||||
style: text-info
|
||||
icon: fa-gear
|
||||
- value: 400
|
||||
style: text-warning
|
||||
icon: fa-circle-question
|
||||
- value: 300
|
||||
style: text-muted
|
||||
icon: fa-thumbs-down
|
||||
- value: 200
|
||||
style: text-muted
|
||||
icon: fa-circle-exclamation
|
||||
- value: 100
|
||||
style: text-muted
|
||||
icon: fa-circle-stop
|
||||
- value: 0
|
||||
style: text-muted
|
||||
icon: fa-ban
|
||||
- value: -1
|
||||
style: text-muted
|
||||
icon: fa-circle-xmark
|
||||
# default configuration
|
||||
- value: default
|
||||
style: text-muted
|
||||
icon: fa-circle-xmark
|
||||
|
||||
# Default Language in which the UI will be rendered if the user's browser language is not an active language
|
||||
defaultLanguage: en
|
||||
@@ -169,6 +202,9 @@ languages:
|
||||
- code: en
|
||||
label: English
|
||||
active: true
|
||||
- code: ar
|
||||
label: العربية
|
||||
active: true
|
||||
- code: ca
|
||||
label: Català
|
||||
active: true
|
||||
@@ -272,6 +308,8 @@ homePage:
|
||||
# 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
|
||||
# Enable or disable the Discover filters on the homepage
|
||||
showDiscoverFilters: false
|
||||
|
||||
# Item Config
|
||||
item:
|
||||
@@ -285,8 +323,17 @@ item:
|
||||
# settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
|
||||
pageSize: 5
|
||||
|
||||
# Community Page Config
|
||||
community:
|
||||
# Search tab config
|
||||
searchSection:
|
||||
showSidebar: true
|
||||
|
||||
# Collection Page Config
|
||||
collection:
|
||||
# Search tab config
|
||||
searchSection:
|
||||
showSidebar: true
|
||||
edit:
|
||||
undoTimeout: 10000 # 10 seconds
|
||||
|
||||
@@ -363,10 +410,11 @@ mediaViewer:
|
||||
|
||||
# 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.
|
||||
# And whether the privacy statement/COAR notify support page should exist or not.
|
||||
info:
|
||||
enableEndUserAgreement: true
|
||||
enablePrivacyStatement: true
|
||||
enableCOARNotifySupport: 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.
|
||||
@@ -392,3 +440,69 @@ comcolSelectionSort:
|
||||
# - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af
|
||||
# source: "openaire"
|
||||
|
||||
|
||||
# Search settings
|
||||
search:
|
||||
# Settings to enable/disable or configure advanced search filters.
|
||||
advancedFilters:
|
||||
enabled: false
|
||||
# List of filters to enable in "Advanced Search" dropdown
|
||||
filter: [ 'title', 'author', 'subject', 'entityType' ]
|
||||
|
||||
|
||||
# Notify metrics
|
||||
# Configuration for Notify Admin Dashboard for metrics visualization
|
||||
notifyMetrics:
|
||||
# Configuration for received messages
|
||||
- title: 'admin-notify-dashboard.received-ldn'
|
||||
boxes:
|
||||
- color: '#B8DAFF'
|
||||
title: 'admin-notify-dashboard.NOTIFY.incoming.accepted'
|
||||
config: 'NOTIFY.incoming.accepted'
|
||||
description: 'admin-notify-dashboard.NOTIFY.incoming.accepted.description'
|
||||
- color: '#D4EDDA'
|
||||
title: 'admin-notify-dashboard.NOTIFY.incoming.processed'
|
||||
config: 'NOTIFY.incoming.processed'
|
||||
description: 'admin-notify-dashboard.NOTIFY.incoming.processed.description'
|
||||
- color: '#FDBBC7'
|
||||
title: 'admin-notify-dashboard.NOTIFY.incoming.failure'
|
||||
config: 'NOTIFY.incoming.failure'
|
||||
description: 'admin-notify-dashboard.NOTIFY.incoming.failure.description'
|
||||
- color: '#FDBBC7'
|
||||
title: 'admin-notify-dashboard.NOTIFY.incoming.untrusted'
|
||||
config: 'NOTIFY.incoming.untrusted'
|
||||
description: 'admin-notify-dashboard.NOTIFY.incoming.untrusted.description'
|
||||
- color: '#43515F'
|
||||
title: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems'
|
||||
textColor: '#fff'
|
||||
config: 'NOTIFY.incoming.involvedItems'
|
||||
description: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems.description'
|
||||
# Configuration for outgoing messages
|
||||
- title: 'admin-notify-dashboard.generated-ldn'
|
||||
boxes:
|
||||
- color: '#B8DAFF'
|
||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.queued'
|
||||
config: 'NOTIFY.outgoing.queued'
|
||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.queued.description'
|
||||
- color: '#FDEEBB'
|
||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry'
|
||||
config: 'NOTIFY.outgoing.queued_for_retry'
|
||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry.description'
|
||||
- color: '#FDBBC7'
|
||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.failure'
|
||||
config: 'NOTIFY.outgoing.failure'
|
||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.failure.description'
|
||||
- color: '#43515F'
|
||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems'
|
||||
textColor: '#fff'
|
||||
config: 'NOTIFY.outgoing.involvedItems'
|
||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems.description'
|
||||
- color: '#D4EDDA'
|
||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.delivered'
|
||||
config: 'NOTIFY.outgoing.delivered'
|
||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description'
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -1,28 +1,28 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
import { Options } from 'cypress-axe';
|
||||
|
||||
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'));
|
||||
});
|
||||
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();
|
||||
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});
|
||||
// Click on every expandable section to open all menus
|
||||
cy.get('ds-expandable-admin-sidebar-section').click({ multiple: true });
|
||||
|
||||
// Analyze <ds-admin-sidebar> for accessibility
|
||||
testA11y('ds-admin-sidebar',
|
||||
// Analyze <ds-admin-sidebar> 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 },
|
||||
}
|
||||
rules: {
|
||||
// Currently all expandable sections have nested interactive elements
|
||||
// See https://github.com/DSpace/dspace-angular/issues/2178
|
||||
'nested-interactive': { enabled: false },
|
||||
},
|
||||
} as Options);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,14 +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')));
|
||||
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');
|
||||
// Wait for breadcrumbs to be visible
|
||||
cy.get('ds-breadcrumbs').should('be.visible');
|
||||
|
||||
// Analyze <ds-breadcrumbs> for accessibility
|
||||
testA11y('ds-breadcrumbs');
|
||||
});
|
||||
// Analyze <ds-breadcrumbs> for accessibility
|
||||
testA11y('ds-breadcrumbs');
|
||||
});
|
||||
});
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Browse By Author', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/author');
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/author');
|
||||
|
||||
// Wait for <ds-browse-by-metadata-page> to be visible
|
||||
cy.get('ds-browse-by-metadata').should('be.visible');
|
||||
// Wait for <ds-browse-by-metadata-page> to be visible
|
||||
cy.get('ds-browse-by-metadata').should('be.visible');
|
||||
|
||||
// Analyze <ds-browse-by-metadata-page> for accessibility
|
||||
testA11y('ds-browse-by-metadata');
|
||||
});
|
||||
// Analyze <ds-browse-by-metadata-page> for accessibility
|
||||
testA11y('ds-browse-by-metadata');
|
||||
});
|
||||
});
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Browse By Date Issued', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/dateissued');
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/dateissued');
|
||||
|
||||
// Wait for <ds-browse-by-date-page> to be visible
|
||||
cy.get('ds-browse-by-date').should('be.visible');
|
||||
// Wait for <ds-browse-by-date-page> to be visible
|
||||
cy.get('ds-browse-by-date').should('be.visible');
|
||||
|
||||
// Analyze <ds-browse-by-date-page> for accessibility
|
||||
testA11y('ds-browse-by-date');
|
||||
});
|
||||
// Analyze <ds-browse-by-date-page> for accessibility
|
||||
testA11y('ds-browse-by-date');
|
||||
});
|
||||
});
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Browse By Subject', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/subject');
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/subject');
|
||||
|
||||
// Wait for <ds-browse-by-metadata-page> to be visible
|
||||
cy.get('ds-browse-by-metadata').should('be.visible');
|
||||
// Wait for <ds-browse-by-metadata-page> to be visible
|
||||
cy.get('ds-browse-by-metadata').should('be.visible');
|
||||
|
||||
// Analyze <ds-browse-by-metadata-page> for accessibility
|
||||
testA11y('ds-browse-by-metadata');
|
||||
});
|
||||
// Analyze <ds-browse-by-metadata-page> for accessibility
|
||||
testA11y('ds-browse-by-metadata');
|
||||
});
|
||||
});
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Browse By Title', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/title');
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/title');
|
||||
|
||||
// Wait for <ds-browse-by-title-page> to be visible
|
||||
cy.get('ds-browse-by-title').should('be.visible');
|
||||
// Wait for <ds-browse-by-title-page> to be visible
|
||||
cy.get('ds-browse-by-title').should('be.visible');
|
||||
|
||||
// Analyze <ds-browse-by-title-page> for accessibility
|
||||
testA11y('ds-browse-by-title');
|
||||
});
|
||||
// Analyze <ds-browse-by-title-page> for accessibility
|
||||
testA11y('ds-browse-by-title');
|
||||
});
|
||||
});
|
||||
|
13
cypress/e2e/collection-create.cy.ts
Normal file
13
cypress/e2e/collection-create.cy.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
beforeEach(() => {
|
||||
cy.visit('/collections/create?parent='.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||
});
|
||||
|
||||
it('should show loading component while saving', () => {
|
||||
const title = 'Test Collection Title';
|
||||
cy.get('#title').type(title);
|
||||
|
||||
cy.get('button[type="submit"]').click();
|
||||
|
||||
cy.get('ds-loading').should('be.visible');
|
||||
});
|
@@ -3,126 +3,126 @@ 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);
|
||||
// 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'));
|
||||
// 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', () => {
|
||||
// <ds-edit-collection> tag must be loaded
|
||||
cy.get('ds-edit-collection').should('be.visible');
|
||||
it('should pass accessibility tests', () => {
|
||||
// <ds-edit-collection> tag must be loaded
|
||||
cy.get('ds-edit-collection').should('be.visible');
|
||||
|
||||
// Analyze <ds-edit-collection> for accessibility issues
|
||||
testA11y('ds-edit-collection');
|
||||
});
|
||||
// Analyze <ds-edit-collection> 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="roles"]').click();
|
||||
|
||||
// <ds-collection-roles> tag must be loaded
|
||||
cy.get('ds-collection-roles').should('be.visible');
|
||||
// <ds-collection-roles> tag must be loaded
|
||||
cy.get('ds-collection-roles').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-collection-roles');
|
||||
});
|
||||
// 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="source"]').click();
|
||||
|
||||
// <ds-collection-source> tag must be loaded
|
||||
cy.get('ds-collection-source').should('be.visible');
|
||||
// <ds-collection-source> 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();
|
||||
// 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');
|
||||
// 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');
|
||||
});
|
||||
// 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="curate"]').click();
|
||||
|
||||
// <ds-collection-curate> tag must be loaded
|
||||
cy.get('ds-collection-curate').should('be.visible');
|
||||
// <ds-collection-curate> tag must be loaded
|
||||
cy.get('ds-collection-curate').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-collection-curate');
|
||||
});
|
||||
// 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="access-control"]').click();
|
||||
|
||||
// <ds-collection-access-control> tag must be loaded
|
||||
cy.get('ds-collection-access-control').should('be.visible');
|
||||
// <ds-collection-access-control> tag must be loaded
|
||||
cy.get('ds-collection-access-control').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-collection-access-control');
|
||||
});
|
||||
// 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="authorizations"]').click();
|
||||
|
||||
// <ds-collection-authorizations> tag must be loaded
|
||||
cy.get('ds-collection-authorizations').should('be.visible');
|
||||
// <ds-collection-authorizations> tag must be loaded
|
||||
cy.get('ds-collection-authorizations').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-collection-authorizations');
|
||||
});
|
||||
// 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="mapper"]').click();
|
||||
|
||||
// <ds-collection-item-mapper> tag must be loaded
|
||||
cy.get('ds-collection-item-mapper').should('be.visible');
|
||||
// <ds-collection-item-mapper> tag must be loaded
|
||||
cy.get('ds-collection-item-mapper').should('be.visible');
|
||||
|
||||
// Analyze entire page for accessibility issues
|
||||
testA11y('ds-collection-item-mapper');
|
||||
// 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();
|
||||
// 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');
|
||||
// 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');
|
||||
});
|
||||
// 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="delete-button"]').click();
|
||||
|
||||
// <ds-delete-collection> tag must be loaded
|
||||
cy.get('ds-delete-collection').should('be.visible');
|
||||
// <ds-delete-collection> tag must be loaded
|
||||
cy.get('ds-delete-collection').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-delete-collection');
|
||||
});
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-delete-collection');
|
||||
});
|
||||
});
|
||||
|
@@ -2,13 +2,13 @@ import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Collection Page', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
|
||||
|
||||
// <ds-collection-page> tag must be loaded
|
||||
cy.get('ds-collection-page').should('be.visible');
|
||||
// <ds-collection-page> tag must be loaded
|
||||
cy.get('ds-collection-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-collection-page> for accessibility issues
|
||||
testA11y('ds-collection-page');
|
||||
});
|
||||
// Analyze <ds-collection-page> for accessibility issues
|
||||
testA11y('ds-collection-page');
|
||||
});
|
||||
});
|
||||
|
@@ -2,36 +2,36 @@ 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'));
|
||||
const COLLECTIONSTATISTICSPAGE = '/statistics/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'));
|
||||
|
||||
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[title="Statistics"]').click();
|
||||
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
|
||||
});
|
||||
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" 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 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);
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||
|
||||
// <ds-collection-statistics-page> tag must be loaded
|
||||
cy.get('ds-collection-statistics-page').should('be.visible');
|
||||
// <ds-collection-statistics-page> 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);
|
||||
// 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 <ds-collection-statistics-page> for accessibility issues
|
||||
testA11y('ds-collection-statistics-page');
|
||||
});
|
||||
// Analyze <ds-collection-statistics-page> for accessibility issues
|
||||
testA11y('ds-collection-statistics-page');
|
||||
});
|
||||
});
|
||||
|
13
cypress/e2e/community-create.cy.ts
Normal file
13
cypress/e2e/community-create.cy.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
beforeEach(() => {
|
||||
cy.visit('/communities/create');
|
||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||
});
|
||||
|
||||
it('should show loading component while saving', () => {
|
||||
const title = 'Test Community Title';
|
||||
cy.get('#title').type(title);
|
||||
|
||||
cy.get('button[type="submit"]').click();
|
||||
|
||||
cy.get('ds-loading').should('be.visible');
|
||||
});
|
@@ -3,84 +3,84 @@ 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);
|
||||
// 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'));
|
||||
// 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', () => {
|
||||
// <ds-edit-community> tag must be loaded
|
||||
cy.get('ds-edit-community').should('be.visible');
|
||||
it('should pass accessibility tests', () => {
|
||||
// <ds-edit-community> tag must be loaded
|
||||
cy.get('ds-edit-community').should('be.visible');
|
||||
|
||||
// Analyze <ds-edit-community> for accessibility issues
|
||||
testA11y('ds-edit-community');
|
||||
});
|
||||
// Analyze <ds-edit-community> 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="roles"]').click();
|
||||
|
||||
// <ds-community-roles> tag must be loaded
|
||||
cy.get('ds-community-roles').should('be.visible');
|
||||
// <ds-community-roles> tag must be loaded
|
||||
cy.get('ds-community-roles').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-community-roles');
|
||||
});
|
||||
// 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="curate"]').click();
|
||||
|
||||
// <ds-community-curate> tag must be loaded
|
||||
cy.get('ds-community-curate').should('be.visible');
|
||||
// <ds-community-curate> tag must be loaded
|
||||
cy.get('ds-community-curate').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-community-curate');
|
||||
});
|
||||
// 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="access-control"]').click();
|
||||
|
||||
// <ds-community-access-control> tag must be loaded
|
||||
cy.get('ds-community-access-control').should('be.visible');
|
||||
// <ds-community-access-control> tag must be loaded
|
||||
cy.get('ds-community-access-control').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-community-access-control');
|
||||
});
|
||||
// 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="authorizations"]').click();
|
||||
|
||||
// <ds-community-authorizations> tag must be loaded
|
||||
cy.get('ds-community-authorizations').should('be.visible');
|
||||
// <ds-community-authorizations> tag must be loaded
|
||||
cy.get('ds-community-authorizations').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-community-authorizations');
|
||||
});
|
||||
// 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();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="delete-button"]').click();
|
||||
|
||||
// <ds-delete-community> tag must be loaded
|
||||
cy.get('ds-delete-community').should('be.visible');
|
||||
// <ds-delete-community> tag must be loaded
|
||||
cy.get('ds-delete-community').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-delete-community');
|
||||
});
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-delete-community');
|
||||
});
|
||||
});
|
||||
|
@@ -2,16 +2,16 @@ import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Community List Page', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/community-list');
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/community-list');
|
||||
|
||||
// <ds-community-list-page> tag must be loaded
|
||||
cy.get('ds-community-list-page').should('be.visible');
|
||||
// <ds-community-list-page> 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 });
|
||||
// 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 <ds-community-list-page> for accessibility issues
|
||||
testA11y('ds-community-list-page');
|
||||
});
|
||||
// Analyze <ds-community-list-page> for accessibility issues
|
||||
testA11y('ds-community-list-page');
|
||||
});
|
||||
});
|
||||
|
@@ -2,13 +2,13 @@ import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Community Page', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
||||
|
||||
// <ds-community-page> tag must be loaded
|
||||
cy.get('ds-community-page').should('be.visible');
|
||||
// <ds-community-page> tag must be loaded
|
||||
cy.get('ds-community-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-community-page> for accessibility issues
|
||||
testA11y('ds-community-page');
|
||||
});
|
||||
// Analyze <ds-community-page> for accessibility issues
|
||||
testA11y('ds-community-page');
|
||||
});
|
||||
});
|
||||
|
@@ -2,36 +2,36 @@ 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'));
|
||||
const COMMUNITYSTATISTICSPAGE = '/statistics/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'));
|
||||
|
||||
it('should load if you click on "Statistics" from a Community page', () => {
|
||||
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
||||
});
|
||||
it('should load if you click on "Statistics" from a Community page', () => {
|
||||
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
||||
cy.get('ds-navbar ds-link-menu-item 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" 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 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);
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||
|
||||
// <ds-community-statistics-page> tag must be loaded
|
||||
cy.get('ds-community-statistics-page').should('be.visible');
|
||||
// <ds-community-statistics-page> 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);
|
||||
// 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 <ds-community-statistics-page> for accessibility issues
|
||||
testA11y('ds-community-statistics-page');
|
||||
});
|
||||
// Analyze <ds-community-statistics-page> for accessibility issues
|
||||
testA11y('ds-community-statistics-page');
|
||||
});
|
||||
});
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Footer', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/');
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/');
|
||||
|
||||
// Footer must first be visible
|
||||
cy.get('ds-footer').should('be.visible');
|
||||
// Footer must first be visible
|
||||
cy.get('ds-footer').should('be.visible');
|
||||
|
||||
// Analyze <ds-footer> for accessibility
|
||||
testA11y('ds-footer');
|
||||
});
|
||||
// Analyze <ds-footer> for accessibility
|
||||
testA11y('ds-footer');
|
||||
});
|
||||
});
|
||||
|
@@ -1,13 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Header', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/');
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/');
|
||||
|
||||
// Header must first be visible
|
||||
cy.get('ds-header').should('be.visible');
|
||||
// Header must first be visible
|
||||
cy.get('ds-header').should('be.visible');
|
||||
|
||||
// Analyze <ds-header> for accessibility
|
||||
testA11y('ds-header');
|
||||
});
|
||||
// Analyze <ds-header> for accessibility
|
||||
testA11y('ds-header');
|
||||
});
|
||||
});
|
||||
|
@@ -1,31 +1,32 @@
|
||||
import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
import '../support/commands';
|
||||
|
||||
import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
|
||||
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 load if you click on "Statistics" from homepage', () => {
|
||||
cy.visit('/');
|
||||
cy.get('ds-navbar ds-link-menu-item 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');
|
||||
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');
|
||||
cy.visit('/statistics');
|
||||
|
||||
// <ds-site-statistics-page> tag must be visable
|
||||
cy.get('ds-site-statistics-page').should('be.visible');
|
||||
// <ds-site-statistics-page> 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);
|
||||
// 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 <ds-site-statistics-page> for accessibility issues
|
||||
testA11y('ds-site-statistics-page');
|
||||
});
|
||||
// Analyze <ds-site-statistics-page> for accessibility issues
|
||||
testA11y('ds-site-statistics-page');
|
||||
});
|
||||
});
|
||||
|
@@ -1,135 +1,135 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
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);
|
||||
// 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'));
|
||||
// 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"]').click();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="metadata"]').click();
|
||||
|
||||
// <ds-edit-item-page> tag must be loaded
|
||||
cy.get('ds-edit-item-page').should('be.visible');
|
||||
// <ds-edit-item-page> tag must be loaded
|
||||
cy.get('ds-edit-item-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-edit-item-page> for accessibility issues
|
||||
testA11y('ds-edit-item-page');
|
||||
});
|
||||
// Analyze <ds-edit-item-page> for accessibility issues
|
||||
testA11y('ds-edit-item-page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit Item > Status tab', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="status"]').click();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="status"]').click();
|
||||
|
||||
// <ds-item-status> tag must be loaded
|
||||
cy.get('ds-item-status').should('be.visible');
|
||||
// <ds-item-status> tag must be loaded
|
||||
cy.get('ds-item-status').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-item-status');
|
||||
});
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-item-status');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit Item > Bitstreams tab', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="bitstreams"]').click();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="bitstreams"]').click();
|
||||
|
||||
// <ds-item-bitstreams> tag must be loaded
|
||||
cy.get('ds-item-bitstreams').should('be.visible');
|
||||
// <ds-item-bitstreams> 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');
|
||||
// Table of item bitstreams must also be loaded
|
||||
cy.get('div.item-bitstreams').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-item-bitstreams',
|
||||
// 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
|
||||
);
|
||||
});
|
||||
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"]').click();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="curate"]').click();
|
||||
|
||||
// <ds-item-curate> tag must be loaded
|
||||
cy.get('ds-item-curate').should('be.visible');
|
||||
// <ds-item-curate> tag must be loaded
|
||||
cy.get('ds-item-curate').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-item-curate');
|
||||
});
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-item-curate');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit Item > Relationships tab', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="relationships"]').click();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="relationships"]').click();
|
||||
|
||||
// <ds-item-relationships> tag must be loaded
|
||||
cy.get('ds-item-relationships').should('be.visible');
|
||||
// <ds-item-relationships> tag must be loaded
|
||||
cy.get('ds-item-relationships').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-item-relationships');
|
||||
});
|
||||
// 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"]').click();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="versionhistory"]').click();
|
||||
|
||||
// <ds-item-version-history> tag must be loaded
|
||||
cy.get('ds-item-version-history').should('be.visible');
|
||||
// <ds-item-version-history> tag must be loaded
|
||||
cy.get('ds-item-version-history').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-item-version-history');
|
||||
});
|
||||
// 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"]').click();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="access-control"]').click();
|
||||
|
||||
// <ds-item-access-control> tag must be loaded
|
||||
cy.get('ds-item-access-control').should('be.visible');
|
||||
// <ds-item-access-control> tag must be loaded
|
||||
cy.get('ds-item-access-control').should('be.visible');
|
||||
|
||||
// Analyze for accessibility issues
|
||||
testA11y('ds-item-access-control');
|
||||
});
|
||||
// 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"]').click();
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.get('a[data-test="mapper"]').click();
|
||||
|
||||
// <ds-item-collection-mapper> tag must be loaded
|
||||
cy.get('ds-item-collection-mapper').should('be.visible');
|
||||
// <ds-item-collection-mapper> tag must be loaded
|
||||
cy.get('ds-item-collection-mapper').should('be.visible');
|
||||
|
||||
// Analyze entire page for accessibility issues
|
||||
testA11y('ds-item-collection-mapper');
|
||||
// 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();
|
||||
// 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');
|
||||
// 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');
|
||||
});
|
||||
// Analyze entire page (again) for accessibility issues
|
||||
testA11y('ds-item-collection-mapper');
|
||||
});
|
||||
});
|
||||
|
@@ -1,32 +1,32 @@
|
||||
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'));
|
||||
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.location('pathname').should('eq', ENTITYPAGE);
|
||||
});
|
||||
// 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);
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(ENTITYPAGE);
|
||||
|
||||
// <ds-item-page> tag must be loaded
|
||||
cy.get('ds-item-page').should('be.visible');
|
||||
// <ds-item-page> tag must be loaded
|
||||
cy.get('ds-item-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-item-page> for accessibility issues
|
||||
testA11y('ds-item-page');
|
||||
});
|
||||
// Analyze <ds-item-page> for accessibility issues
|
||||
testA11y('ds-item-page');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests on full item page', () => {
|
||||
cy.visit(ENTITYPAGE + '/full');
|
||||
it('should pass accessibility tests on full item page', () => {
|
||||
cy.visit(ENTITYPAGE + '/full');
|
||||
|
||||
// <ds-full-item-page> tag must be loaded
|
||||
cy.get('ds-full-item-page').should('be.visible');
|
||||
// <ds-full-item-page> tag must be loaded
|
||||
cy.get('ds-full-item-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-full-item-page> for accessibility issues
|
||||
testA11y('ds-full-item-page');
|
||||
});
|
||||
// Analyze <ds-full-item-page> for accessibility issues
|
||||
testA11y('ds-full-item-page');
|
||||
});
|
||||
});
|
||||
|
@@ -2,42 +2,42 @@ 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'));
|
||||
const ITEMSTATISTICSPAGE = '/statistics/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
|
||||
|
||||
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('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
||||
});
|
||||
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('ds-navbar ds-link-menu-item 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 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" 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);
|
||||
// Check just for existence because this table is empty in CI environment as it's historical data
|
||||
cy.get('.'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('_TotalVisitsPerMonth')).should('exist');
|
||||
});
|
||||
it('should contain a "Total visits per month" section', () => {
|
||||
cy.visit(ITEMSTATISTICSPAGE);
|
||||
// Check just for existence because this table is empty in CI environment as it's historical data
|
||||
cy.get('.'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('_TotalVisitsPerMonth')).should('exist');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(ITEMSTATISTICSPAGE);
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(ITEMSTATISTICSPAGE);
|
||||
|
||||
// <ds-item-statistics-page> tag must be loaded
|
||||
cy.get('ds-item-statistics-page').should('be.visible');
|
||||
// <ds-item-statistics-page> 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);
|
||||
// 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 <ds-item-statistics-page> for accessibility issues
|
||||
testA11y('ds-item-statistics-page');
|
||||
});
|
||||
// Analyze <ds-item-statistics-page> for accessibility issues
|
||||
testA11y('ds-item-statistics-page');
|
||||
});
|
||||
});
|
||||
|
15
cypress/e2e/item-template.cy.ts
Normal file
15
cypress/e2e/item-template.cy.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
const ADD_TEMPLATE_ITEM_PAGE = '/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('/itemtemplate');
|
||||
|
||||
describe('Item Template', () => {
|
||||
beforeEach(() => {
|
||||
cy.visit(ADD_TEMPLATE_ITEM_PAGE);
|
||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||
});
|
||||
|
||||
it('should load properly', () => {
|
||||
cy.contains('.ds-header-row .lbl-cell', 'Field', { timeout: 10000 }).should('exist').should('be.visible');
|
||||
cy.contains('.ds-header-row b', 'Value', { timeout: 10000 }).should('exist').should('be.visible');
|
||||
cy.contains('.ds-header-row b', 'Lang', { timeout: 10000 }).should('exist').should('be.visible');
|
||||
cy.contains('.ds-header-row b', 'Edit', { timeout: 10000 }).should('exist').should('be.visible');
|
||||
});
|
||||
});
|
@@ -1,150 +1,150 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
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');
|
||||
}
|
||||
openLoginMenu() {
|
||||
// Click the "Log In" dropdown menu in header
|
||||
cy.get('ds-header [data-test="login-menu"]').click();
|
||||
},
|
||||
openUserMenu() {
|
||||
// Once logged in, click the User menu in header
|
||||
cy.get('ds-header [data-test="user-menu"]').click();
|
||||
},
|
||||
submitLoginAndPasswordByPressingButton(email, password) {
|
||||
// Enter email
|
||||
cy.get('ds-header [data-test="email"]').type(email);
|
||||
// Enter password
|
||||
cy.get('ds-header [data-test="password"]').type(password);
|
||||
// Click login button
|
||||
cy.get('ds-header [data-test="login-button"]').click();
|
||||
},
|
||||
submitLoginAndPasswordByPressingEnter(email, password) {
|
||||
// In opened Login modal, fill out email & password, then click Enter
|
||||
cy.get('ds-header [data-test="email"]').type(email);
|
||||
cy.get('ds-header [data-test="password"]').type(password);
|
||||
cy.get('ds-header [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-header [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/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
|
||||
cy.visit(ENTITYPAGE);
|
||||
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 menu should exist
|
||||
cy.get('ds-log-in').should('exist');
|
||||
|
||||
// Login, and the <ds-log-in> tag should no longer exist
|
||||
page.openLoginMenu();
|
||||
cy.get('.form-login').should('be.visible');
|
||||
// Login, and the <ds-log-in> 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');
|
||||
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);
|
||||
// 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');
|
||||
});
|
||||
// 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');
|
||||
it('should login when clicking enter key & stay on same page', () => {
|
||||
cy.visit('/home');
|
||||
|
||||
// Open login menu in header & verify <ds-log-in> tag is visible
|
||||
page.openLoginMenu();
|
||||
cy.get('.form-login').should('be.visible');
|
||||
// Open login menu in header & verify <ds-log-in> tag is visible
|
||||
page.openLoginMenu();
|
||||
cy.get('.form-login').should('be.visible');
|
||||
|
||||
// Login, and the <ds-log-in> tag should no longer exist
|
||||
page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||
cy.get('.form-login').should('not.exist');
|
||||
// Login, and the <ds-log-in> tag should no longer exist
|
||||
page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||
cy.get('.form-login').should('not.exist');
|
||||
|
||||
// Verify we are still on homepage
|
||||
cy.url().should('include', '/home');
|
||||
// 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');
|
||||
});
|
||||
// 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('/');
|
||||
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');
|
||||
// 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();
|
||||
// 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');
|
||||
});
|
||||
// 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('/');
|
||||
it('should allow new user registration', () => {
|
||||
cy.visit('/');
|
||||
|
||||
page.openLoginMenu();
|
||||
page.openLoginMenu();
|
||||
|
||||
// Registration link should be visible
|
||||
cy.get('ds-themed-navbar [data-test="register"]').should('be.visible');
|
||||
// Registration link should be visible
|
||||
cy.get('ds-header [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');
|
||||
// Click registration link & you should go to registration page
|
||||
cy.get('ds-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');
|
||||
});
|
||||
// Test accessibility of this page
|
||||
testA11y('ds-register-email');
|
||||
});
|
||||
|
||||
it('should allow forgot password', () => {
|
||||
cy.visit('/');
|
||||
it('should allow forgot password', () => {
|
||||
cy.visit('/');
|
||||
|
||||
page.openLoginMenu();
|
||||
page.openLoginMenu();
|
||||
|
||||
// Forgot password link should be visible
|
||||
cy.get('ds-themed-navbar [data-test="forgot"]').should('be.visible');
|
||||
// Forgot password link should be visible
|
||||
cy.get('ds-header [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');
|
||||
// Click link & you should go to Forgot Password page
|
||||
cy.get('ds-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');
|
||||
});
|
||||
// Test accessibility of this page
|
||||
testA11y('ds-forgot-email');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests in menus', () => {
|
||||
cy.visit('/');
|
||||
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');
|
||||
// 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');
|
||||
// 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');
|
||||
});
|
||||
// Open user menu, verify user menu accesibility
|
||||
page.openUserMenu();
|
||||
cy.get('ds-user-menu').should('be.visible');
|
||||
testA11y('ds-user-menu');
|
||||
});
|
||||
});
|
||||
|
@@ -1,134 +1,134 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('My DSpace page', () => {
|
||||
it('should display recent submissions and pass accessibility tests', () => {
|
||||
cy.visit('/mydspace');
|
||||
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'));
|
||||
// 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');
|
||||
cy.get('ds-my-dspace-page').should('be.visible');
|
||||
|
||||
// At least one recent submission should be displayed
|
||||
cy.get('[data-test="list-object"]').should('be.visible');
|
||||
// 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 });
|
||||
// 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 <ds-my-dspace-page> for accessibility issues
|
||||
testA11y('ds-my-dspace-page');
|
||||
// Analyze <ds-my-dspace-page> for accessibility issues
|
||||
testA11y('ds-my-dspace-page');
|
||||
});
|
||||
|
||||
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');
|
||||
|
||||
// Click button in sidebar to display detailed view
|
||||
cy.get('ds-search-sidebar [data-test="detail-view"]').click();
|
||||
|
||||
cy.get('ds-object-detail').should('be.visible');
|
||||
|
||||
// Analyze <ds-my-dspace-page> 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 <ds-create-item-parent-selector> (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');
|
||||
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
|
||||
it('should have a working detailed view that passes accessibility tests', () => {
|
||||
cy.visit('/mydspace');
|
||||
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'));
|
||||
// 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');
|
||||
// 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();
|
||||
|
||||
// Click button in sidebar to display detailed view
|
||||
cy.get('ds-search-sidebar [data-test="detail-view"]').click();
|
||||
// New URL should include /import-external, as we've moved to the import page
|
||||
cy.url().should('include', '/import-external');
|
||||
|
||||
cy.get('ds-object-detail').should('be.visible');
|
||||
// The external import searchbox should be visible
|
||||
cy.get('ds-submission-import-external-searchbar').should('be.visible');
|
||||
|
||||
// Analyze <ds-my-dspace-page> 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 <ds-create-item-parent-selector> (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');
|
||||
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
// Test for accessibility issues
|
||||
testA11y('ds-submission-import-external');
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,18 +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');
|
||||
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 <ds-pagenotfound> for accessibility issues
|
||||
testA11y('ds-pagenotfound');
|
||||
});
|
||||
// Analyze <ds-pagenotfound> 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');
|
||||
});
|
||||
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
|
||||
cy.visit('/home');
|
||||
cy.get('ds-pagenotfound').should('not.exist');
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -1,64 +1,64 @@
|
||||
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();
|
||||
}
|
||||
fillOutQueryInNavBar(query) {
|
||||
// Click the magnifying glass
|
||||
cy.get('ds-header [data-test="header-search-icon"]').click();
|
||||
// Fill out a query in input that appears
|
||||
cy.get('ds-header [data-test="header-search-box"]').type(query);
|
||||
},
|
||||
submitQueryByPressingEnter() {
|
||||
cy.get('ds-header [data-test="header-search-box"]').type('{enter}');
|
||||
},
|
||||
submitQueryByPressingIcon() {
|
||||
cy.get('ds-header [data-test="header-search-icon"]').click();
|
||||
},
|
||||
};
|
||||
|
||||
describe('Search from Navigation Bar', () => {
|
||||
// NOTE: these tests currently assume this query will return results!
|
||||
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
|
||||
// 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 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 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');
|
||||
});
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
@@ -1,57 +1,57 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
import { Options } from 'cypress-axe';
|
||||
|
||||
describe('Search Page', () => {
|
||||
// NOTE: these tests currently assume this query will return results!
|
||||
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
|
||||
// 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));
|
||||
});
|
||||
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='.concat(query));
|
||||
cy.get('[data-test="search-box"]').should('have.value', query);
|
||||
it('should load results and pass accessibility tests', () => {
|
||||
cy.visit('/search?query='.concat(query));
|
||||
cy.get('[data-test="search-box"]').should('have.value', query);
|
||||
|
||||
// <ds-search-page> tag must be loaded
|
||||
cy.get('ds-search-page').should('be.visible');
|
||||
// <ds-search-page> 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');
|
||||
// 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 });
|
||||
// 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 <ds-search-page> for accessibility issues
|
||||
testA11y('ds-search-page');
|
||||
});
|
||||
// Analyze <ds-search-page> for accessibility issues
|
||||
testA11y('ds-search-page');
|
||||
});
|
||||
|
||||
it('should have a working grid view that passes accessibility tests', () => {
|
||||
cy.visit('/search?query='.concat(query));
|
||||
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();
|
||||
// Click button in sidebar to display grid view
|
||||
cy.get('ds-search-sidebar [data-test="grid-view"]').click();
|
||||
|
||||
// <ds-search-page> tag must be loaded
|
||||
cy.get('ds-search-page').should('be.visible');
|
||||
// <ds-search-page> 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');
|
||||
// At least one grid object (card) should be displayed
|
||||
cy.get('[data-test="grid-object"]').should('be.visible');
|
||||
|
||||
// Analyze <ds-search-page> for accessibility issues
|
||||
testA11y('ds-search-page',
|
||||
// Analyze <ds-search-page> for accessibility issues
|
||||
testA11y('ds-search-page',
|
||||
{
|
||||
rules: {
|
||||
// Card titles fail this test currently
|
||||
'heading-order': { enabled: false }
|
||||
}
|
||||
} as Options
|
||||
);
|
||||
});
|
||||
rules: {
|
||||
// Card titles fail this test currently
|
||||
'heading-order': { enabled: false },
|
||||
},
|
||||
} as Options,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@@ -4,224 +4,224 @@ import { Options } from 'cypress-axe';
|
||||
|
||||
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'));
|
||||
// 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_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
|
||||
// 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'));
|
||||
|
||||
// Should redirect to /workspaceitems, as we've started a new submission
|
||||
cy.url().should('include', '/workspaceitems');
|
||||
// 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');
|
||||
// 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'));
|
||||
// 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');
|
||||
// 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
|
||||
testA11y('ds-submission-edit',
|
||||
// Test entire page for accessibility
|
||||
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 },
|
||||
}
|
||||
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
|
||||
);
|
||||
} 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();
|
||||
// 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_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_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');
|
||||
|
||||
// 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_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_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 <span>)
|
||||
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',
|
||||
});
|
||||
|
||||
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'));
|
||||
// Wait for upload to complete before proceeding
|
||||
cy.wait('@upload');
|
||||
|
||||
// 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'));
|
||||
// Wait for deposit button to not be disabled & click it.
|
||||
cy.get('button#deposit').should('not.be.disabled').click();
|
||||
|
||||
// Attempt an immediate deposit without filling out any fields
|
||||
cy.get('button#deposit').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');
|
||||
});
|
||||
|
||||
// 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');
|
||||
it('is possible to submit a new "Person" and that form passes accessibility', () => {
|
||||
// To submit a different entity type, we'll start from MyDSpace
|
||||
cy.visit('/mydspace');
|
||||
|
||||
// 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');
|
||||
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||
// NOTE: At this time, we MUST login as admin to submit Person objects
|
||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||
|
||||
// Title field should have class "is-invalid" applied, as it's required
|
||||
cy.get('input#dc_title').should('have.class', 'is-invalid');
|
||||
// Open the New Submission dropdown
|
||||
cy.get('button[data-test="submission-dropdown"]').click();
|
||||
// Click on the "Person" type in that dropdown
|
||||
cy.get('#entityControlsDropdownMenu button[title="Person"]').click();
|
||||
|
||||
// Date Year field should also have "is-valid" class
|
||||
cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid');
|
||||
// This should display the <ds-create-item-parent-selector> (popup window)
|
||||
cy.get('ds-create-item-parent-selector').should('be.visible');
|
||||
|
||||
// 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];
|
||||
// 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_PERSON_COLLECTION_NAME'));
|
||||
|
||||
// Even though form is incomplete, the "Save for Later" button should still work
|
||||
cy.get('button#saveForLater').click();
|
||||
// Click on the button matching that known Collection name
|
||||
cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME')).concat('"]')).click();
|
||||
|
||||
// "Save for Later" should send us to MyDSpace
|
||||
cy.url().should('include', '/mydspace');
|
||||
// New URL should include /workspaceitems, as we've started a new submission
|
||||
cy.url().should('include', '/workspaceitems');
|
||||
|
||||
// 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});
|
||||
// The Submission edit form tag should be visible
|
||||
cy.get('ds-submission-edit').should('be.visible');
|
||||
|
||||
// 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();
|
||||
// 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_PERSON_COLLECTION_NAME'));
|
||||
|
||||
// Wait for search results to come back from the above GET command
|
||||
cy.wait('@search-results');
|
||||
// 3 sections should be visible by default
|
||||
cy.get('div#section_personStep').should('be.visible');
|
||||
cy.get('div#section_upload').should('be.visible');
|
||||
cy.get('div#section_license').should('be.visible');
|
||||
|
||||
// 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_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_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 <span>)
|
||||
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');
|
||||
|
||||
// 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');
|
||||
});
|
||||
|
||||
it('is possible to submit a new "Person" and that form passes accessibility', () => {
|
||||
// To submit a different entity type, we'll start from MyDSpace
|
||||
cy.visit('/mydspace');
|
||||
|
||||
// This page is restricted, so we will be shown the login form. Fill it out & submit.
|
||||
// NOTE: At this time, we MUST login as admin to submit Person objects
|
||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||
|
||||
// Open the New Submission dropdown
|
||||
cy.get('button[data-test="submission-dropdown"]').click();
|
||||
// Click on the "Person" type in that dropdown
|
||||
cy.get('#entityControlsDropdownMenu button[title="Person"]').click();
|
||||
|
||||
// This should display the <ds-create-item-parent-selector> (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_PERSON_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_PERSON_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_PERSON_COLLECTION_NAME'));
|
||||
|
||||
// 3 sections should be visible by default
|
||||
cy.get('div#section_personStep').should('be.visible');
|
||||
cy.get('div#section_upload').should('be.visible');
|
||||
cy.get('div#section_license').should('be.visible');
|
||||
|
||||
// Test entire page for accessibility
|
||||
testA11y('ds-submission-edit',
|
||||
// Test entire page for accessibility
|
||||
testA11y('ds-submission-edit',
|
||||
{
|
||||
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 },
|
||||
}
|
||||
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
|
||||
);
|
||||
} as Options,
|
||||
);
|
||||
|
||||
// Click the lookup button next to "Publication" field
|
||||
cy.get('button[data-test="lookup-button"]').click();
|
||||
// Click the lookup button next to "Publication" field
|
||||
cy.get('button[data-test="lookup-button"]').click();
|
||||
|
||||
// A popup modal window should be visible
|
||||
cy.get('ds-dynamic-lookup-relation-modal').should('be.visible');
|
||||
// A popup modal window should be visible
|
||||
cy.get('ds-dynamic-lookup-relation-modal').should('be.visible');
|
||||
|
||||
// Popup modal should also pass accessibility tests
|
||||
//testA11y('ds-dynamic-lookup-relation-modal');
|
||||
testA11y({
|
||||
include: ['ds-dynamic-lookup-relation-modal'],
|
||||
exclude: [
|
||||
['ul.nav-tabs'] // Tabs at top of model have several issues which seem to be caused by ng-bootstrap
|
||||
],
|
||||
});
|
||||
|
||||
// Close popup window
|
||||
cy.get('ds-dynamic-lookup-relation-modal button.close').click();
|
||||
|
||||
// Back on the form, click the discard button to remove new submission
|
||||
// Clicking it will display a confirmation, which we will confirm with another click
|
||||
cy.get('button#discard').click();
|
||||
cy.get('button#discard_submit').click();
|
||||
// Popup modal should also pass accessibility tests
|
||||
//testA11y('ds-dynamic-lookup-relation-modal');
|
||||
testA11y({
|
||||
include: ['ds-dynamic-lookup-relation-modal'],
|
||||
exclude: [
|
||||
['ul.nav-tabs'], // Tabs at top of model have several issues which seem to be caused by ng-bootstrap
|
||||
],
|
||||
});
|
||||
|
||||
// Close popup window
|
||||
cy.get('ds-dynamic-lookup-relation-modal button.close').click();
|
||||
|
||||
// Back on the form, click the discard button to remove new submission
|
||||
// Clicking it will display a confirmation, which we will confirm with another click
|
||||
cy.get('button#discard').click();
|
||||
cy.get('button#discard_submit').click();
|
||||
});
|
||||
});
|
||||
|
@@ -9,51 +9,51 @@ 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) => {
|
||||
on('task', {
|
||||
// Define "log" and "table" tasks, used for logging accessibility errors during CI
|
||||
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
|
||||
log(message: string) {
|
||||
console.log(message);
|
||||
return null;
|
||||
},
|
||||
table(message: string) {
|
||||
console.table(message);
|
||||
return null;
|
||||
},
|
||||
// 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.
|
||||
readUIConfig() {
|
||||
// Check if we have a config.json in the src/assets. If so, use that.
|
||||
// This is where it's written when running "ng e2e" or "yarn serve"
|
||||
if (fs.existsSync('./src/assets/config.json')) {
|
||||
return fs.readFileSync('./src/assets/config.json', 'utf8');
|
||||
// Otherwise, check the dist/browser/assets
|
||||
// This is where it's written when running "serve:ssr", which is what CI uses to start the frontend
|
||||
} else if (fs.existsSync('./dist/browser/assets/config.json')) {
|
||||
return fs.readFileSync('./dist/browser/assets/config.json', 'utf8');
|
||||
}
|
||||
on('task', {
|
||||
// Define "log" and "table" tasks, used for logging accessibility errors during CI
|
||||
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
|
||||
log(message: string) {
|
||||
console.log(message);
|
||||
return null;
|
||||
},
|
||||
table(message: string) {
|
||||
console.table(message);
|
||||
return null;
|
||||
},
|
||||
// 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.
|
||||
readUIConfig() {
|
||||
// Check if we have a config.json in the src/assets. If so, use that.
|
||||
// This is where it's written when running "ng e2e" or "yarn serve"
|
||||
if (fs.existsSync('./src/assets/config.json')) {
|
||||
return fs.readFileSync('./src/assets/config.json', 'utf8');
|
||||
// Otherwise, check the dist/browser/assets
|
||||
// This is where it's written when running "serve:ssr", which is what CI uses to start the frontend
|
||||
} else if (fs.existsSync('./dist/browser/assets/config.json')) {
|
||||
return fs.readFileSync('./dist/browser/assets/config.json', 'utf8');
|
||||
}
|
||||
|
||||
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 ;
|
||||
}
|
||||
});
|
||||
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 ;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@@ -3,8 +3,14 @@
|
||||
// See docs at https://docs.cypress.io/api/cypress-api/custom-commands
|
||||
// ***********************************************
|
||||
|
||||
import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
|
||||
import { DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER } from 'src/app/core/xsrf/xsrf.constants';
|
||||
import {
|
||||
AuthTokenInfo,
|
||||
TOKENITEM,
|
||||
} from 'src/app/core/auth/models/auth-token-info.model';
|
||||
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
|
||||
@@ -57,33 +63,33 @@ declare global {
|
||||
* @param password password to login as
|
||||
*/
|
||||
function login(email: string, password: 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 login POST request including that CSRF token
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: baseRestUrl + '/api/authn/login',
|
||||
headers: { [XSRF_REQUEST_HEADER]: csrfToken},
|
||||
form: true, // indicates the body should be form urlencoded
|
||||
body: { user: email, password: password }
|
||||
}).then((resp) => {
|
||||
// We expect a successful login
|
||||
expect(resp.status).to.eq(200);
|
||||
// We expect to have a valid authorization header returned (with our auth token)
|
||||
expect(resp.headers).to.have.property('authorization');
|
||||
// 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: { [XSRF_REQUEST_HEADER]: csrfToken },
|
||||
form: true, // indicates the body should be form urlencoded
|
||||
body: { user: email, password: password },
|
||||
}).then((resp) => {
|
||||
// We expect a successful login
|
||||
expect(resp.status).to.eq(200);
|
||||
// We expect to have a valid authorization header returned (with our auth token)
|
||||
expect(resp.headers).to.have.property('authorization');
|
||||
|
||||
// Initialize our AuthTokenInfo object from the authorization header.
|
||||
const authheader = resp.headers.authorization as string;
|
||||
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
|
||||
// Initialize our AuthTokenInfo object from the authorization header.
|
||||
const authheader = resp.headers.authorization as string;
|
||||
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
|
||||
|
||||
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
|
||||
// This ensures the UI will recognize we are logged in on next "visit()"
|
||||
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
|
||||
});
|
||||
});
|
||||
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
|
||||
// This ensures the UI will recognize we are logged in on next "visit()"
|
||||
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
// Add as a Cypress command (i.e. assign to 'cy.login')
|
||||
Cypress.Commands.add('login', login);
|
||||
@@ -94,12 +100,12 @@ Cypress.Commands.add('login', login);
|
||||
* @param password password to login as
|
||||
*/
|
||||
function loginViaForm(email: string, password: string): void {
|
||||
// Enter email
|
||||
cy.get('ds-log-in [data-test="email"]').type(email);
|
||||
// Enter password
|
||||
cy.get('ds-log-in [data-test="password"]').type(password);
|
||||
// Click login button
|
||||
cy.get('ds-log-in [data-test="login-button"]').click();
|
||||
// Enter email
|
||||
cy.get('ds-log-in [data-test="email"]').type(email);
|
||||
// Enter password
|
||||
cy.get('ds-log-in [data-test="password"]').type(password);
|
||||
// Click login button
|
||||
cy.get('ds-log-in [data-test="login-button"]').click();
|
||||
}
|
||||
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
|
||||
Cypress.Commands.add('loginViaForm', loginViaForm);
|
||||
@@ -117,29 +123,29 @@ Cypress.Commands.add('loginViaForm', loginViaForm);
|
||||
* @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);
|
||||
});
|
||||
});
|
||||
// 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);
|
||||
@@ -153,17 +159,17 @@ Cypress.Commands.add('generateViewEvent', generateViewEvent);
|
||||
* @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();
|
||||
// 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 });
|
||||
});
|
||||
// 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);
|
||||
// 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);
|
||||
|
@@ -15,12 +15,11 @@
|
||||
|
||||
// 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';
|
||||
|
||||
import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants';
|
||||
|
||||
// Runs once before all tests
|
||||
before(() => {
|
||||
@@ -35,18 +34,18 @@ before(() => {
|
||||
// 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);
|
||||
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
|
||||
} else {
|
||||
baseRestUrl = config.rest.baseUrl;
|
||||
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);
|
||||
console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN);
|
||||
} else {
|
||||
baseDomain = config.rest.host;
|
||||
baseDomain = config.rest.host;
|
||||
}
|
||||
cy.task('saveRestBaseDomain', baseDomain);
|
||||
|
||||
@@ -55,12 +54,12 @@ before(() => {
|
||||
|
||||
// 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}');
|
||||
// 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);
|
||||
// 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
|
||||
|
@@ -5,26 +5,26 @@ import { Options } from 'cypress-axe';
|
||||
// Uses 'log' and 'table' tasks defined in ../plugins/index.ts
|
||||
// Borrowed from https://github.com/component-driven/cypress-axe#in-your-spec-file
|
||||
function terminalLog(violations: Result[]) {
|
||||
cy.task(
|
||||
'log',
|
||||
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`
|
||||
);
|
||||
// pluck specific keys to keep the table readable
|
||||
const violationData = violations.map(
|
||||
({ id, impact, description, helpUrl, nodes }) => ({
|
||||
id,
|
||||
impact,
|
||||
description,
|
||||
helpUrl,
|
||||
nodes: nodes.length,
|
||||
html: nodes.map(node => node.html)
|
||||
})
|
||||
);
|
||||
cy.task(
|
||||
'log',
|
||||
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`,
|
||||
);
|
||||
// pluck specific keys to keep the table readable
|
||||
const violationData = violations.map(
|
||||
({ id, impact, description, helpUrl, nodes }) => ({
|
||||
id,
|
||||
impact,
|
||||
description,
|
||||
helpUrl,
|
||||
nodes: nodes.length,
|
||||
html: nodes.map(node => node.html),
|
||||
}),
|
||||
);
|
||||
|
||||
// Print violations as an array, since 'node.html' above often breaks table alignment
|
||||
cy.task('log', violationData);
|
||||
// Optionally, uncomment to print as a table
|
||||
// cy.task('table', violationData);
|
||||
// Print violations as an array, since 'node.html' above often breaks table alignment
|
||||
cy.task('log', violationData);
|
||||
// Optionally, uncomment to print as a table
|
||||
// cy.task('table', violationData);
|
||||
|
||||
}
|
||||
|
||||
@@ -32,13 +32,13 @@ function terminalLog(violations: Result[]) {
|
||||
// while also ensuring any violations are logged to the terminal (see terminalLog above)
|
||||
// This method MUST be called after cy.visit(), as cy.injectAxe() must be called after page load
|
||||
export const testA11y = (context?: any, options?: Options) => {
|
||||
cy.injectAxe();
|
||||
cy.configureAxe({
|
||||
rules: [
|
||||
// Disable color contrast checks as they are inaccurate / result in a lot of false positives
|
||||
// See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast
|
||||
{ id: 'color-contrast', enabled: false },
|
||||
]
|
||||
});
|
||||
cy.checkA11y(context, options, terminalLog);
|
||||
cy.injectAxe();
|
||||
cy.configureAxe({
|
||||
rules: [
|
||||
// Disable color contrast checks as they are inaccurate / result in a lot of false positives
|
||||
// See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast
|
||||
{ id: 'color-contrast', enabled: false },
|
||||
],
|
||||
});
|
||||
cy.checkA11y(context, options, terminalLog);
|
||||
};
|
||||
|
@@ -4,10 +4,11 @@
|
||||
"**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"sourceMap": false,
|
||||
"types": [
|
||||
"cypress",
|
||||
"cypress-axe",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,7 +20,7 @@ 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'
|
||||
This Dockerfile is used to build a *development* DSpace Angular UI image, published as 'dspace/dspace-angular'
|
||||
|
||||
```
|
||||
docker build -t dspace/dspace-angular:latest .
|
||||
@@ -46,11 +46,11 @@ 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.
|
||||
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace REST instance will also be started in Docker.
|
||||
- docker-compose-rest.yml
|
||||
- Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
|
||||
- Runs a published instance of the DSpace REST API - persists data in Docker volumes
|
||||
- docker-compose-ci.yml
|
||||
- Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup.
|
||||
- Runs a published instance of the DSpace REST API for CI testing. The database is re-populated from a SQL dump on each startup.
|
||||
- cli.yml
|
||||
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||
- cli.assetstore.yml
|
||||
@@ -71,7 +71,7 @@ docker-compose -f docker/docker-compose.yml build
|
||||
|
||||
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
|
||||
docker-compose -p d8 -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.
|
||||
@@ -86,14 +86,14 @@ _The system will be started in 2 steps. Each step shares the same docker network
|
||||
|
||||
From 'DSpace/DSpace' clone (build first as needed):
|
||||
```
|
||||
docker-compose -p d7 up -d
|
||||
docker-compose -p d8 up -d
|
||||
```
|
||||
|
||||
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
|
||||
docker-compose -p d8 -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
At this point, you should be able to access the UI from http://localhost:4000,
|
||||
@@ -107,19 +107,19 @@ This allows you to run the Angular UI in *production* mode, pointing it at the d
|
||||
```
|
||||
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
|
||||
docker-compose -p d8 -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 d8 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
|
||||
```
|
||||
|
||||
Load content from AIP files
|
||||
```
|
||||
docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
|
||||
docker-compose -p d8 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
|
||||
```
|
||||
|
||||
## Alternative Ingest - Use Entities dataset
|
||||
@@ -127,12 +127,12 @@ _Delete your docker volumes or use a unique project (-p) name_
|
||||
|
||||
Start DSpace with Database Content from a database dump
|
||||
```
|
||||
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
|
||||
docker-compose -p d8 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
|
||||
```
|
||||
|
||||
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
|
||||
docker-compose -p d8 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
|
||||
```
|
||||
|
||||
## End to end testing of the REST API (runs in GitHub Actions CI).
|
||||
@@ -140,5 +140,5 @@ _In this instance, only the REST api runs in Docker using the Entities dataset.
|
||||
|
||||
This command is only really useful for testing our Continuous Integration process.
|
||||
```
|
||||
docker-compose -p d7ci -f docker/docker-compose-ci.yml up -d
|
||||
docker-compose -p d8ci -f docker/docker-compose-ci.yml up -d
|
||||
```
|
||||
|
@@ -12,8 +12,6 @@
|
||||
# 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"
|
||||
|
||||
services:
|
||||
dspace-cli:
|
||||
environment:
|
||||
|
@@ -12,8 +12,6 @@
|
||||
# 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:
|
||||
@@ -34,5 +32,7 @@ services:
|
||||
|
||||
/dspace/bin/dspace packager -r -a -t AIP -e $${ADMIN_EMAIL} -f -u SITE*.zip
|
||||
/dspace/bin/dspace database update-sequences
|
||||
touch /dspace/solr/search/conf/reindex.flag
|
||||
|
||||
/dspace/bin/dspace index-discovery
|
||||
/dspace/bin/dspace oai import
|
||||
/dspace/bin/dspace oai clean-cache
|
||||
|
@@ -12,7 +12,6 @@
|
||||
# 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")
|
||||
|
@@ -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: dspace/dspace-postgres-pgcrypto::${DSPACE_VER:-latest}-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
|
||||
@@ -29,23 +27,11 @@ services:
|
||||
# 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
|
||||
# 4. Finally, start Tomcat
|
||||
# 4. Finally, start DSpace
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||
/dspace/bin/dspace database migrate ignored
|
||||
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
|
||||
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
|
||||
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
|
||||
<name-map collection-handle="123456789/5" submission-name="Publication"/> \
|
||||
<name-map collection-handle="123456789/8" submission-name="OrgUnit"/> \
|
||||
<name-map collection-handle="123456789/6" submission-name="Person"/> \
|
||||
<name-map collection-handle="123456789/279" submission-name="Person"/> \
|
||||
<name-map collection-handle="123456789/7" submission-name="Project"/> \
|
||||
<name-map collection-handle="123456789/280" submission-name="Project"/> \
|
||||
<name-map collection-handle="123456789/28" submission-name="Journal"/> \
|
||||
<name-map collection-handle="123456789/29" submission-name="JournalVolume"/> \
|
||||
<name-map collection-handle="123456789/30" submission-name="JournalIssue"/>' /dspace/config/item-submission.xml
|
||||
catalina.sh run
|
||||
java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace
|
@@ -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:
|
||||
@@ -33,6 +32,7 @@ services:
|
||||
# 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
|
||||
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
||||
depends_on:
|
||||
- dspacedb
|
||||
@@ -48,27 +48,31 @@ services:
|
||||
# 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 (including any out-of-order ignored migrations, if any)
|
||||
# 3. Finally, start Tomcat
|
||||
# 3. Finally, start DSpace
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||
/dspace/bin/dspace database migrate ignored
|
||||
catalina.sh run
|
||||
java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace
|
||||
# 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: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}-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
|
||||
PGDATA: /pgdata
|
||||
image: dspace/dspace-postgres-pgcrypto:loadsql
|
||||
POSTGRES_PASSWORD: dspace
|
||||
networks:
|
||||
- dspacenet
|
||||
ports:
|
||||
- published: 5432
|
||||
target: 5432
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
@@ -105,6 +109,8 @@ services:
|
||||
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
|
||||
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
|
||||
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
|
||||
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
|
||||
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
|
||||
exec solr -f
|
||||
volumes:
|
||||
assetstore:
|
||||
|
@@ -8,7 +8,6 @@
|
||||
|
||||
# Docker Compose for running the DSpace Angular UI dist build
|
||||
# for previewing with the DSpace Demo site backend
|
||||
version: '3.7'
|
||||
networks:
|
||||
dspacenet:
|
||||
services:
|
||||
|
@@ -10,7 +10,6 @@
|
||||
# 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:
|
||||
ipam:
|
||||
@@ -29,8 +28,9 @@ services:
|
||||
# __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
|
||||
# Uncomment to set a non-default value for dspace.server.url or dspace.ui.url
|
||||
# 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'
|
||||
# db.url: Ensure we are using the 'dspacedb' image for our database
|
||||
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
||||
@@ -39,6 +39,7 @@ services:
|
||||
# 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'
|
||||
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
||||
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
||||
depends_on:
|
||||
- dspacedb
|
||||
@@ -50,24 +51,27 @@ services:
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
# Keep DSpace assetstore directory between reboots
|
||||
- assetstore:/dspace/assetstore
|
||||
# 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. Finally, start DSpace
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||
/dspace/bin/dspace database migrate
|
||||
catalina.sh run
|
||||
java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace
|
||||
# DSpace database container
|
||||
dspacedb:
|
||||
container_name: dspacedb
|
||||
# Uses a custom Postgres image with pgcrypto installed
|
||||
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
|
||||
environment:
|
||||
PGDATA: /pgdata
|
||||
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
|
||||
POSTGRES_PASSWORD: dspace
|
||||
networks:
|
||||
- dspacenet
|
||||
ports:
|
||||
@@ -113,6 +117,8 @@ services:
|
||||
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
|
||||
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
|
||||
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
|
||||
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
|
||||
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
|
||||
exec solr -f
|
||||
volumes:
|
||||
assetstore:
|
||||
|
@@ -9,7 +9,6 @@
|
||||
# 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:
|
||||
|
4
docs/lint/html/index.md
Normal file
4
docs/lint/html/index.md
Normal file
@@ -0,0 +1,4 @@
|
||||
[DSpace ESLint plugins](../../../lint/README.md) > HTML rules
|
||||
_______
|
||||
|
||||
- [`dspace-angular-html/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via the selector of their `ThemedComponent` wrapper class
|
110
docs/lint/html/rules/themed-component-usages.md
Normal file
110
docs/lint/html/rules/themed-component-usages.md
Normal file
@@ -0,0 +1,110 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [HTML rules](../index.md) > `dspace-angular-html/themed-component-usages`
|
||||
_______
|
||||
|
||||
Themeable components should be used via the selector of their `ThemedComponent` wrapper class
|
||||
|
||||
This ensures that custom themes can correctly override _all_ instances of this component.
|
||||
The only exception to this rule are unit tests, where we may want to use the base component in order to keep the test setup simple.
|
||||
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/html/themed-component-usages.ts)
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Valid code
|
||||
|
||||
##### use no-prefix selectors in HTML templates
|
||||
|
||||
```html
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
```
|
||||
|
||||
##### use no-prefix selectors in TypeScript templates
|
||||
|
||||
```html
|
||||
@Component({
|
||||
template: '<ds-test-themeable></ds-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
```
|
||||
|
||||
##### use no-prefix selectors in TypeScript test templates
|
||||
|
||||
Filename: `lint/test/fixture/src/test.spec.ts`
|
||||
|
||||
```html
|
||||
@Component({
|
||||
template: '<ds-test-themeable></ds-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
```
|
||||
|
||||
##### base selectors are also allowed in TypeScript test templates
|
||||
|
||||
Filename: `lint/test/fixture/src/test.spec.ts`
|
||||
|
||||
```html
|
||||
@Component({
|
||||
template: '<ds-base-test-themeable></ds-base-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
#### Invalid code & automatic fixes
|
||||
|
||||
##### themed override selectors are not allowed in HTML templates
|
||||
|
||||
```html
|
||||
<ds-themed-test-themeable/>
|
||||
<ds-themed-test-themeable></ds-themed-test-themeable>
|
||||
<ds-themed-test-themeable [test]="something"></ds-themed-test-themeable>
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```html
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
```
|
||||
|
||||
|
||||
##### base selectors are not allowed in HTML templates
|
||||
|
||||
```html
|
||||
<ds-base-test-themeable/>
|
||||
<ds-base-test-themeable></ds-base-test-themeable>
|
||||
<ds-base-test-themeable [test]="something"></ds-base-test-themeable>
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```html
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
```
|
||||
|
||||
|
||||
|
6
docs/lint/ts/index.md
Normal file
6
docs/lint/ts/index.md
Normal file
@@ -0,0 +1,6 @@
|
||||
[DSpace ESLint plugins](../../../lint/README.md) > TypeScript rules
|
||||
_______
|
||||
|
||||
- [`dspace-angular-ts/themed-component-classes`](./rules/themed-component-classes.md): Formatting rules for themeable component classes
|
||||
- [`dspace-angular-ts/themed-component-selectors`](./rules/themed-component-selectors.md): Themeable component selectors should follow the DSpace convention
|
||||
- [`dspace-angular-ts/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via their `ThemedComponent` wrapper class
|
257
docs/lint/ts/rules/themed-component-classes.md
Normal file
257
docs/lint/ts/rules/themed-component-classes.md
Normal file
@@ -0,0 +1,257 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-component-classes`
|
||||
_______
|
||||
|
||||
Formatting rules for themeable component classes
|
||||
|
||||
- All themeable components must be standalone.
|
||||
- The base component must always be imported in the `ThemedComponent` wrapper. This ensures that it is always sufficient to import just the wrapper whenever we use the component.
|
||||
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/ts/themed-component-classes.ts)
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Valid code
|
||||
|
||||
##### Regular non-themeable component
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
standalone: true,
|
||||
})
|
||||
class Something {
|
||||
}
|
||||
```
|
||||
|
||||
##### Base component
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-base-test-themable',
|
||||
standalone: true,
|
||||
})
|
||||
class TestThemeableTomponent {
|
||||
}
|
||||
```
|
||||
|
||||
##### Wrapper component
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [
|
||||
TestThemeableComponent,
|
||||
],
|
||||
})
|
||||
class ThemedTestThemeableTomponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
|
||||
##### Override component
|
||||
|
||||
Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-themed-test-themable',
|
||||
standalone: true,
|
||||
})
|
||||
class Override extends BaseComponent {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
#### Invalid code & automatic fixes
|
||||
|
||||
##### Base component must be standalone
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-base-test-themable',
|
||||
})
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components must be standalone
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-base-test-themable',
|
||||
standalone: true,
|
||||
})
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Wrapper component must be standalone and import base component
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable component wrapper classes must be standalone and import the base class
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Wrapper component must import base component (array present but empty)
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themed component wrapper classes must only import the base class
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Wrapper component must import base component (array is wrong)
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
import { SomethingElse } from './somewhere-else';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [
|
||||
SomethingElse,
|
||||
],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themed component wrapper classes must only import the base class
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
import { SomethingElse } from './somewhere-else';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Wrapper component must import base component (array is wrong)
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
import { Something, SomethingElse } from './somewhere-else';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [
|
||||
SomethingElse,
|
||||
],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themed component wrapper classes must only import the base class
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
import { Something, SomethingElse } from './somewhere-else';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Override component must be standalone
|
||||
|
||||
Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-themed-test-themable',
|
||||
})
|
||||
class Override extends BaseComponent {
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components must be standalone
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-themed-test-themable',
|
||||
standalone: true,
|
||||
})
|
||||
class Override extends BaseComponent {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
156
docs/lint/ts/rules/themed-component-selectors.md
Normal file
156
docs/lint/ts/rules/themed-component-selectors.md
Normal file
@@ -0,0 +1,156 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-component-selectors`
|
||||
_______
|
||||
|
||||
Themeable component selectors should follow the DSpace convention
|
||||
|
||||
Each themeable component is comprised of a base component, a wrapper component and any number of themed components
|
||||
- Base components should have a selector starting with `ds-base-`
|
||||
- Themed components should have a selector starting with `ds-themed-`
|
||||
- Wrapper components should have a selector starting with `ds-`, but not `ds-base-` or `ds-themed-`
|
||||
- This is the regular DSpace selector prefix
|
||||
- **When making a regular component themeable, its selector prefix should be changed to `ds-base-`, and the new wrapper's component should reuse the previous selector**
|
||||
|
||||
Unit tests are exempt from this rule, because they may redefine components using the same class name as other themeable components elsewhere in the source.
|
||||
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/ts/themed-component-selectors.ts)
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Valid code
|
||||
|
||||
##### Regular non-themeable component selector
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class Something {
|
||||
}
|
||||
```
|
||||
|
||||
##### Themeable component selector should replace the original version, unthemed version should be changed to ds-base-
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-base-something',
|
||||
})
|
||||
class Something {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class ThemedSomething extends ThemedComponent<Something> {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-something',
|
||||
})
|
||||
class OverrideSomething extends Something {
|
||||
}
|
||||
```
|
||||
|
||||
##### Other themed component wrappers should not interfere
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class Something {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-something-else',
|
||||
})
|
||||
class ThemedSomethingElse extends ThemedComponent<SomethingElse> {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
#### Invalid code & automatic fixes
|
||||
|
||||
##### Wrong selector for base component
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/test-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Unthemed version of themeable component should have a selector starting with 'ds-base-'
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-base-something',
|
||||
})
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Wrong selector for wrapper component
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-themed-something',
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themed component wrapper of themeable component shouldn't have a selector starting with 'ds-themed-'
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Wrong selector for theme override
|
||||
|
||||
Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class TestThememeableComponent extends BaseComponent {
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Theme override of themeable component should have a selector starting with 'ds-themed-'
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-themed-something',
|
||||
})
|
||||
class TestThememeableComponent extends BaseComponent {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
332
docs/lint/ts/rules/themed-component-usages.md
Normal file
332
docs/lint/ts/rules/themed-component-usages.md
Normal file
@@ -0,0 +1,332 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-component-usages`
|
||||
_______
|
||||
|
||||
Themeable components should be used via their `ThemedComponent` wrapper class
|
||||
|
||||
This ensures that custom themes can correctly override _all_ instances of this component.
|
||||
There are a few exceptions where the base class can still be used:
|
||||
- Class declaration expressions (otherwise we can't declare, extend or override the class in the first place)
|
||||
- Angular modules (except for routing modules)
|
||||
- Angular `@ViewChild` decorators
|
||||
- Type annotations
|
||||
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/ts/themed-component-usages.ts)
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Valid code
|
||||
|
||||
##### allow wrapper class usages
|
||||
|
||||
```typescript
|
||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
||||
|
||||
const config = {
|
||||
a: ThemedTestThemeableComponent,
|
||||
b: ChipsComponent,
|
||||
}
|
||||
```
|
||||
|
||||
##### allow base class in class declaration
|
||||
|
||||
```typescript
|
||||
export class TestThemeableComponent {
|
||||
}
|
||||
```
|
||||
|
||||
##### allow inheriting from base class
|
||||
|
||||
```typescript
|
||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
||||
|
||||
export class ThemedAdminSidebarComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
|
||||
##### allow base class in ViewChild
|
||||
|
||||
```typescript
|
||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
||||
|
||||
export class Something {
|
||||
@ViewChild(TestThemeableComponent) test: TestThemeableComponent;
|
||||
}
|
||||
```
|
||||
|
||||
##### allow wrapper selectors in test queries
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/test.component.spec.ts`
|
||||
|
||||
```typescript
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
```
|
||||
|
||||
##### allow wrapper selectors in cypress queries
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/test.component.cy.ts`
|
||||
|
||||
```typescript
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
#### Invalid code & automatic fixes
|
||||
|
||||
##### disallow direct usages of base class
|
||||
|
||||
```typescript
|
||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
||||
import { TestComponent } from './app/test/test.component';
|
||||
|
||||
const config = {
|
||||
a: TestThemeableComponent,
|
||||
b: TestComponent,
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
||||
import { TestComponent } from './app/test/test.component';
|
||||
|
||||
const config = {
|
||||
a: ThemedTestThemeableComponent,
|
||||
b: TestComponent,
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### disallow direct usages of base class, keep other imports
|
||||
|
||||
```typescript
|
||||
import { Something, TestThemeableComponent } from './app/test/test-themeable.component';
|
||||
import { TestComponent } from './app/test/test.component';
|
||||
|
||||
const config = {
|
||||
a: TestThemeableComponent,
|
||||
b: TestComponent,
|
||||
c: Something,
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
import { Something } from './app/test/test-themeable.component';
|
||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
||||
import { TestComponent } from './app/test/test.component';
|
||||
|
||||
const config = {
|
||||
a: ThemedTestThemeableComponent,
|
||||
b: TestComponent,
|
||||
c: Something,
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### handle array replacements correctly
|
||||
|
||||
```typescript
|
||||
const DECLARATIONS = [
|
||||
Something,
|
||||
TestThemeableComponent,
|
||||
Something,
|
||||
ThemedTestThemeableComponent,
|
||||
];
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
const DECLARATIONS = [
|
||||
Something,
|
||||
Something,
|
||||
ThemedTestThemeableComponent,
|
||||
];
|
||||
```
|
||||
|
||||
|
||||
##### disallow override selector in test queries
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/test.component.spec.ts`
|
||||
|
||||
```typescript
|
||||
By.css('ds-themed-themeable');
|
||||
By.css('#test > ds-themed-themeable > #nest');
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
```
|
||||
|
||||
|
||||
##### disallow base selector in test queries
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/test.component.spec.ts`
|
||||
|
||||
```typescript
|
||||
By.css('ds-base-themeable');
|
||||
By.css('#test > ds-base-themeable > #nest');
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
```
|
||||
|
||||
|
||||
##### disallow override selector in cypress queries
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/test.component.cy.ts`
|
||||
|
||||
```typescript
|
||||
cy.get('ds-themed-themeable');
|
||||
cy.get('#test > ds-themed-themeable > #nest');
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
cy.get('ds-themeable');
|
||||
cy.get('#test > ds-themeable > #nest');
|
||||
```
|
||||
|
||||
|
||||
##### disallow base selector in cypress queries
|
||||
|
||||
Filename: `lint/test/fixture/src/app/test/test.component.cy.ts`
|
||||
|
||||
```typescript
|
||||
cy.get('ds-base-themeable');
|
||||
cy.get('#test > ds-base-themeable > #nest');
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
cy.get('ds-themeable');
|
||||
cy.get('#test > ds-themeable > #nest');
|
||||
```
|
||||
|
||||
|
||||
##### edge case: unable to find usage node through usage token, but import is still flagged and fixed
|
||||
|
||||
Filename: `lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { Context } from './app/core/shared/context.model';
|
||||
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent],
|
||||
})
|
||||
export class UsageComponent {
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { Context } from './app/core/shared/context.model';
|
||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [ThemedTestThemeableComponent],
|
||||
})
|
||||
export class UsageComponent {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### edge case edge case: both are imported, only wrapper is retained
|
||||
|
||||
Filename: `lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts`
|
||||
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { Context } from './app/core/shared/context.model';
|
||||
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
|
||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent, ThemedTestThemeableComponent],
|
||||
})
|
||||
export class UsageComponent {
|
||||
}
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
Themeable components should be used via their ThemedComponent wrapper
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { Context } from './app/core/shared/context.model';
|
||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [ThemedTestThemeableComponent],
|
||||
})
|
||||
export class UsageComponent {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
@@ -15,7 +15,10 @@ module.exports = function (config) {
|
||||
],
|
||||
client: {
|
||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||
captureConsole: false
|
||||
captureConsole: false,
|
||||
jasmine: {
|
||||
failSpecWithNoExpectations: true
|
||||
}
|
||||
},
|
||||
coverageIstanbulReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/dspace-angular'),
|
||||
|
3
lint/.gitignore
vendored
Normal file
3
lint/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
/dist/
|
||||
/coverage/
|
||||
/node-modules/
|
50
lint/README.md
Normal file
50
lint/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# DSpace ESLint plugins
|
||||
|
||||
Custom ESLint rules for DSpace Angular peculiarities.
|
||||
|
||||
## Usage
|
||||
|
||||
These plugins are included with the rest of our ESLint configuration in [.eslintc.json](../.eslintrc.json). Individual rules can be configured or disabled there, like usual.
|
||||
- In order for the new rules to be picked up by your IDE, you should first run `yarn build:lint` to build the plugins.
|
||||
- This will also happen automatically each time `yarn lint` is run.
|
||||
|
||||
## Documentation
|
||||
|
||||
The rules are split up into plugins by language:
|
||||
- [TypeScript rules](../docs/lint/ts/index.md)
|
||||
- [HTML rules](../docs/lint/html/index.md)
|
||||
|
||||
> Run `yarn docs:lint` to generate this documentation!
|
||||
|
||||
## Developing
|
||||
|
||||
### Overview
|
||||
|
||||
- All rules are written in TypeScript and compiled into [`dist`](./dist)
|
||||
- The plugins are linked into the main project dependencies from here
|
||||
- These directories already contain the necessary `package.json` files to mark them as ESLint plugins
|
||||
- Rule source files are structured, so they can be imported all in one go
|
||||
- Each rule must export the following:
|
||||
- `Messages`: an Enum of error message IDs
|
||||
- `info`: metadata about this rule (name, description, messages, options, ...)
|
||||
- `rule`: the implementation of the rule
|
||||
- `tests`: the tests for this rule, as a set of valid/invalid code snippets. These snippets are used as example in the documentation.
|
||||
- New rules should be added to their plugin's `index.ts`
|
||||
- Some useful links
|
||||
- [Developing ESLint plugins](https://eslint.org/docs/latest/extend/plugins)
|
||||
- [Custom rules in typescript-eslint](https://typescript-eslint.io/developers/custom-rules)
|
||||
- [Angular ESLint](https://github.com/angular-eslint/angular-eslint)
|
||||
|
||||
### Parsing project metadata in advance ~ TypeScript AST
|
||||
|
||||
While it is possible to retain persistent state between files during the linting process, it becomes quite complicated if the content of one file determines how we want to lint another file.
|
||||
Because the two files may be linted out of order, we may not know whether the first file is wrong before we pass by the second. This means that we cannot report or fix the issue, because the first file is already detached from the linting context.
|
||||
|
||||
For example, we cannot consistently determine which components are themeable (i.e. have a `ThemedComponent` wrapper) while linting.
|
||||
To work around this issue, we construct a registry of themeable components _before_ linting anything.
|
||||
- We don't have a good way to hook into the ESLint parser at this time
|
||||
- Instead, we leverage the actual TypeScript AST parser
|
||||
- Retrieve all `ThemedComponent` wrapper files by the pattern of their path (`themed-*.component.ts`)
|
||||
- Determine the themed component they're linked to (by the actual type annotation/import path, since filenames are prone to errors)
|
||||
- Store metadata describing these component pairs in a global registry that can be shared between rules
|
||||
- This only needs to happen once, and only takes a fraction of a second (for ~100 themeable components)
|
6
lint/dist/src/rules/html/package.json
vendored
Normal file
6
lint/dist/src/rules/html/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "eslint-plugin-dspace-angular-html",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.js",
|
||||
"private": true
|
||||
}
|
6
lint/dist/src/rules/ts/package.json
vendored
Normal file
6
lint/dist/src/rules/ts/package.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "eslint-plugin-dspace-angular-ts",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.js",
|
||||
"private": true
|
||||
}
|
85
lint/generate-docs.ts
Normal file
85
lint/generate-docs.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { default as htmlPlugin } from './src/rules/html';
|
||||
import { default as tsPlugin } from './src/rules/ts';
|
||||
|
||||
const templates = new Map();
|
||||
|
||||
function lazyEJS(path: string, data: object): string {
|
||||
if (!templates.has(path)) {
|
||||
templates.set(path, require('ejs').compile(readFileSync(path).toString()));
|
||||
}
|
||||
|
||||
return templates.get(path)(data).replace(/\r\n/g, '\n');
|
||||
}
|
||||
|
||||
const docsDir = join('docs', 'lint');
|
||||
const tsDir = join(docsDir, 'ts');
|
||||
const htmlDir = join(docsDir, 'html');
|
||||
|
||||
if (existsSync(docsDir)) {
|
||||
rmSync(docsDir, { recursive: true });
|
||||
}
|
||||
|
||||
mkdirSync(join(tsDir, 'rules'), { recursive: true });
|
||||
mkdirSync(join(htmlDir, 'rules'), { recursive: true });
|
||||
|
||||
function template(name: string): string {
|
||||
return join('lint', 'src', 'util', 'templates', name);
|
||||
}
|
||||
|
||||
// TypeScript docs
|
||||
writeFileSync(
|
||||
join(tsDir, 'index.md'),
|
||||
lazyEJS(template('index.ejs'), {
|
||||
plugin: tsPlugin,
|
||||
rules: tsPlugin.index.map(rule => rule.info),
|
||||
}),
|
||||
);
|
||||
|
||||
for (const rule of tsPlugin.index) {
|
||||
writeFileSync(
|
||||
join(tsDir, 'rules', rule.info.name + '.md'),
|
||||
lazyEJS(template('rule.ejs'), {
|
||||
plugin: tsPlugin,
|
||||
rule: rule.info,
|
||||
tests: rule.tests,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// HTML docs
|
||||
writeFileSync(
|
||||
join(htmlDir, 'index.md'),
|
||||
lazyEJS(template('index.ejs'), {
|
||||
plugin: htmlPlugin,
|
||||
rules: htmlPlugin.index.map(rule => rule.info),
|
||||
}),
|
||||
);
|
||||
|
||||
for (const rule of htmlPlugin.index) {
|
||||
writeFileSync(
|
||||
join(htmlDir, 'rules', rule.info.name + '.md'),
|
||||
lazyEJS(template('rule.ejs'), {
|
||||
plugin: htmlPlugin,
|
||||
rule: rule.info,
|
||||
tests: rule.tests,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
7
lint/jasmine.json
Normal file
7
lint/jasmine.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"spec_files": ["**/*.spec.js"],
|
||||
"spec_dir": "lint/dist/test",
|
||||
"helpers": [
|
||||
"./test/helpers.js"
|
||||
]
|
||||
}
|
22
lint/src/rules/html/index.ts
Normal file
22
lint/src/rules/html/index.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
/* eslint-disable import/no-namespace */
|
||||
import {
|
||||
bundle,
|
||||
RuleExports,
|
||||
} from '../../util/structure';
|
||||
import * as themedComponentUsages from './themed-component-usages';
|
||||
|
||||
const index = [
|
||||
themedComponentUsages,
|
||||
] as unknown as RuleExports[];
|
||||
|
||||
export = {
|
||||
parser: require('@angular-eslint/template-parser'),
|
||||
...bundle('dspace-angular-html', 'HTML', index),
|
||||
};
|
191
lint/src/rules/html/themed-component-usages.ts
Normal file
191
lint/src/rules/html/themed-component-usages.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import { TmplAstElement } from '@angular-eslint/bundled-angular-compiler';
|
||||
import { TemplateParserServices } from '@angular-eslint/utils';
|
||||
import {
|
||||
ESLintUtils,
|
||||
TSESLint,
|
||||
} from '@typescript-eslint/utils';
|
||||
|
||||
import { fixture } from '../../../test/fixture';
|
||||
import {
|
||||
DSpaceESLintRuleInfo,
|
||||
NamedTests,
|
||||
} from '../../util/structure';
|
||||
import {
|
||||
DISALLOWED_THEME_SELECTORS,
|
||||
fixSelectors,
|
||||
} from '../../util/theme-support';
|
||||
import {
|
||||
getFilename,
|
||||
getSourceCode,
|
||||
} from '../../util/typescript';
|
||||
|
||||
export enum Message {
|
||||
WRONG_SELECTOR = 'mustUseThemedWrapperSelector',
|
||||
}
|
||||
|
||||
export const info = {
|
||||
name: 'themed-component-usages',
|
||||
meta: {
|
||||
docs: {
|
||||
description: `Themeable components should be used via the selector of their \`ThemedComponent\` wrapper class
|
||||
|
||||
This ensures that custom themes can correctly override _all_ instances of this component.
|
||||
The only exception to this rule are unit tests, where we may want to use the base component in order to keep the test setup simple.
|
||||
`,
|
||||
},
|
||||
type: 'problem',
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
messages: {
|
||||
[Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper\'s selector',
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: TSESLint.RuleContext<Message, unknown[]>) {
|
||||
if (getFilename(context).includes('.spec.ts')) {
|
||||
// skip inline templates in unit tests
|
||||
return {};
|
||||
}
|
||||
|
||||
const parserServices = getSourceCode(context).parserServices as TemplateParserServices;
|
||||
|
||||
return {
|
||||
[`Element$1[name = /^${DISALLOWED_THEME_SELECTORS}/]`](node: TmplAstElement) {
|
||||
const { startSourceSpan, endSourceSpan } = node;
|
||||
const openStart = startSourceSpan.start.offset as number;
|
||||
|
||||
context.report({
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
loc: parserServices.convertNodeSourceSpanToLoc(startSourceSpan),
|
||||
fix(fixer) {
|
||||
const oldSelector = node.name;
|
||||
const newSelector = fixSelectors(oldSelector);
|
||||
|
||||
const ops = [
|
||||
fixer.replaceTextRange([openStart + 1, openStart + 1 + oldSelector.length], newSelector),
|
||||
];
|
||||
|
||||
// make sure we don't mangle self-closing tags
|
||||
if (endSourceSpan !== null && startSourceSpan.end.offset !== endSourceSpan.end.offset) {
|
||||
const closeStart = endSourceSpan.start.offset as number;
|
||||
const closeEnd = endSourceSpan.end.offset as number;
|
||||
|
||||
ops.push(fixer.replaceTextRange([closeStart + 2, closeEnd - 1], newSelector));
|
||||
}
|
||||
|
||||
return ops;
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const tests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'use no-prefix selectors in HTML templates',
|
||||
code: `
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'use no-prefix selectors in TypeScript templates',
|
||||
code: `
|
||||
@Component({
|
||||
template: '<ds-test-themeable></ds-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'use no-prefix selectors in TypeScript test templates',
|
||||
filename: fixture('src/test.spec.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
template: '<ds-test-themeable></ds-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'base selectors are also allowed in TypeScript test templates',
|
||||
filename: fixture('src/test.spec.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
template: '<ds-base-test-themeable></ds-base-test-themeable>'
|
||||
})
|
||||
class Test {
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'themed override selectors are not allowed in HTML templates',
|
||||
code: `
|
||||
<ds-themed-test-themeable/>
|
||||
<ds-themed-test-themeable></ds-themed-test-themeable>
|
||||
<ds-themed-test-themeable [test]="something"></ds-themed-test-themeable>
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'base selectors are not allowed in HTML templates',
|
||||
code: `
|
||||
<ds-base-test-themeable/>
|
||||
<ds-base-test-themeable></ds-base-test-themeable>
|
||||
<ds-base-test-themeable [test]="something"></ds-base-test-themeable>
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
<ds-test-themeable/>
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
`,
|
||||
},
|
||||
],
|
||||
} as NamedTests;
|
||||
|
||||
export default rule;
|
25
lint/src/rules/ts/index.ts
Normal file
25
lint/src/rules/ts/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import {
|
||||
bundle,
|
||||
RuleExports,
|
||||
} from '../../util/structure';
|
||||
/* eslint-disable import/no-namespace */
|
||||
import * as themedComponentClasses from './themed-component-classes';
|
||||
import * as themedComponentSelectors from './themed-component-selectors';
|
||||
import * as themedComponentUsages from './themed-component-usages';
|
||||
|
||||
const index = [
|
||||
themedComponentClasses,
|
||||
themedComponentSelectors,
|
||||
themedComponentUsages,
|
||||
] as unknown as RuleExports[];
|
||||
|
||||
export = {
|
||||
...bundle('dspace-angular-ts', 'TypeScript', index),
|
||||
};
|
382
lint/src/rules/ts/themed-component-classes.ts
Normal file
382
lint/src/rules/ts/themed-component-classes.ts
Normal file
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import {
|
||||
ESLintUtils,
|
||||
TSESLint,
|
||||
TSESTree,
|
||||
} from '@typescript-eslint/utils';
|
||||
|
||||
import { fixture } from '../../../test/fixture';
|
||||
import {
|
||||
getComponentImportNode,
|
||||
getComponentInitializer,
|
||||
getComponentStandaloneNode,
|
||||
} from '../../util/angular';
|
||||
import { appendObjectProperties } from '../../util/fix';
|
||||
import { DSpaceESLintRuleInfo } from '../../util/structure';
|
||||
import {
|
||||
getBaseComponentClassName,
|
||||
inThemedComponentOverrideFile,
|
||||
isThemeableComponent,
|
||||
isThemedComponentWrapper,
|
||||
} from '../../util/theme-support';
|
||||
import { getFilename } from '../../util/typescript';
|
||||
|
||||
export enum Message {
|
||||
NOT_STANDALONE = 'mustBeStandalone',
|
||||
NOT_STANDALONE_IMPORTS_BASE = 'mustBeStandaloneAndImportBase',
|
||||
WRAPPER_IMPORTS_BASE = 'wrapperShouldImportBase',
|
||||
}
|
||||
|
||||
export const info = {
|
||||
name: 'themed-component-classes',
|
||||
meta: {
|
||||
docs: {
|
||||
description: `Formatting rules for themeable component classes
|
||||
|
||||
- All themeable components must be standalone.
|
||||
- The base component must always be imported in the \`ThemedComponent\` wrapper. This ensures that it is always sufficient to import just the wrapper whenever we use the component.
|
||||
`,
|
||||
},
|
||||
type: 'problem',
|
||||
fixable: 'code',
|
||||
schema: [],
|
||||
messages: {
|
||||
[Message.NOT_STANDALONE]: 'Themeable components must be standalone',
|
||||
[Message.NOT_STANDALONE_IMPORTS_BASE]: 'Themeable component wrapper classes must be standalone and import the base class',
|
||||
[Message.WRAPPER_IMPORTS_BASE]: 'Themed component wrapper classes must only import the base class',
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: TSESLint.RuleContext<Message, unknown[]>) {
|
||||
const filename = getFilename(context);
|
||||
|
||||
if (filename.endsWith('.spec.ts')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
function enforceStandalone(decoratorNode: TSESTree.Decorator, withBaseImport = false) {
|
||||
const standaloneNode = getComponentStandaloneNode(decoratorNode);
|
||||
|
||||
if (standaloneNode === undefined) {
|
||||
// We may need to add these properties in one go
|
||||
if (!withBaseImport) {
|
||||
context.report({
|
||||
messageId: Message.NOT_STANDALONE,
|
||||
node: decoratorNode,
|
||||
fix(fixer) {
|
||||
const initializer = getComponentInitializer(decoratorNode);
|
||||
return appendObjectProperties(context, fixer, initializer, ['standalone: true']);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (!standaloneNode.value) {
|
||||
context.report({
|
||||
messageId: Message.NOT_STANDALONE,
|
||||
node: standaloneNode,
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(standaloneNode, 'true');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (withBaseImport) {
|
||||
const baseClass = getBaseComponentClassName(decoratorNode);
|
||||
|
||||
if (baseClass === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importsNode = getComponentImportNode(decoratorNode);
|
||||
|
||||
if (importsNode === undefined) {
|
||||
if (standaloneNode === undefined) {
|
||||
context.report({
|
||||
messageId: Message.NOT_STANDALONE_IMPORTS_BASE,
|
||||
node: decoratorNode,
|
||||
fix(fixer) {
|
||||
const initializer = getComponentInitializer(decoratorNode);
|
||||
return appendObjectProperties(context, fixer, initializer, ['standalone: true', `imports: [${baseClass}]`]);
|
||||
},
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
messageId: Message.WRAPPER_IMPORTS_BASE,
|
||||
node: decoratorNode,
|
||||
fix(fixer) {
|
||||
const initializer = getComponentInitializer(decoratorNode);
|
||||
return appendObjectProperties(context, fixer, initializer, [`imports: [${baseClass}]`]);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// If we have an imports node, standalone: true will be enforced by another rule
|
||||
|
||||
const imports = importsNode.elements.map(e => (e as TSESTree.Identifier).name);
|
||||
|
||||
if (!imports.includes(baseClass) || imports.length > 1) {
|
||||
// The wrapper should _only_ import the base component
|
||||
context.report({
|
||||
messageId: Message.WRAPPER_IMPORTS_BASE,
|
||||
node: importsNode,
|
||||
fix(fixer) {
|
||||
// todo: this may leave unused imports, but that's better than mangling things
|
||||
return fixer.replaceText(importsNode, `[${baseClass}]`);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: TSESTree.Decorator) {
|
||||
const classNode = node.parent as TSESTree.ClassDeclaration;
|
||||
const className = classNode.id?.name;
|
||||
|
||||
if (className === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isThemedComponentWrapper(node)) {
|
||||
enforceStandalone(node, true);
|
||||
} else if (inThemedComponentOverrideFile(filename)) {
|
||||
enforceStandalone(node);
|
||||
} else if (isThemeableComponent(className)) {
|
||||
enforceStandalone(node);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const tests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'Regular non-themeable component',
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
standalone: true,
|
||||
})
|
||||
class Something {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Base component',
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-base-test-themable',
|
||||
standalone: true,
|
||||
})
|
||||
class TestThemeableTomponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Wrapper component',
|
||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [
|
||||
TestThemeableComponent,
|
||||
],
|
||||
})
|
||||
class ThemedTestThemeableTomponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Override component',
|
||||
filename: fixture('src/themes/test/app/test/test-themeable.component.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-themed-test-themable',
|
||||
standalone: true,
|
||||
})
|
||||
class Override extends BaseComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Base component must be standalone',
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-base-test-themable',
|
||||
})
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
`,
|
||||
errors:[
|
||||
{
|
||||
messageId: Message.NOT_STANDALONE,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-base-test-themable',
|
||||
standalone: true,
|
||||
})
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Wrapper component must be standalone and import base component',
|
||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
errors:[
|
||||
{
|
||||
messageId: Message.NOT_STANDALONE_IMPORTS_BASE,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: 'Wrapper component must import base component (array present but empty)',
|
||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
errors:[
|
||||
{
|
||||
messageId: Message.WRAPPER_IMPORTS_BASE,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Wrapper component must import base component (array is wrong)',
|
||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
||||
code: `
|
||||
import { SomethingElse } from './somewhere-else';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [
|
||||
SomethingElse,
|
||||
],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
errors:[
|
||||
{
|
||||
messageId: Message.WRAPPER_IMPORTS_BASE,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
import { SomethingElse } from './somewhere-else';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
}, {
|
||||
name: 'Wrapper component must import base component (array is wrong)',
|
||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
||||
code: `
|
||||
import { Something, SomethingElse } from './somewhere-else';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [
|
||||
SomethingElse,
|
||||
],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
errors:[
|
||||
{
|
||||
messageId: Message.WRAPPER_IMPORTS_BASE,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
import { Something, SomethingElse } from './somewhere-else';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-test-themable',
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent],
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Override component must be standalone',
|
||||
filename: fixture('src/themes/test/app/test/test-themeable.component.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-themed-test-themable',
|
||||
})
|
||||
class Override extends BaseComponent {
|
||||
}
|
||||
`,
|
||||
errors:[
|
||||
{
|
||||
messageId: Message.NOT_STANDALONE,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-themed-test-themable',
|
||||
standalone: true,
|
||||
})
|
||||
class Override extends BaseComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
};
|
257
lint/src/rules/ts/themed-component-selectors.ts
Normal file
257
lint/src/rules/ts/themed-component-selectors.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import {
|
||||
ESLintUtils,
|
||||
TSESLint,
|
||||
TSESTree,
|
||||
} from '@typescript-eslint/utils';
|
||||
|
||||
import { fixture } from '../../../test/fixture';
|
||||
import { getComponentSelectorNode } from '../../util/angular';
|
||||
import { stringLiteral } from '../../util/misc';
|
||||
import { DSpaceESLintRuleInfo } from '../../util/structure';
|
||||
import {
|
||||
inThemedComponentOverrideFile,
|
||||
isThemeableComponent,
|
||||
isThemedComponentWrapper,
|
||||
} from '../../util/theme-support';
|
||||
import { getFilename } from '../../util/typescript';
|
||||
|
||||
export enum Message {
|
||||
BASE = 'wrongSelectorUnthemedComponent',
|
||||
WRAPPER = 'wrongSelectorThemedComponentWrapper',
|
||||
THEMED = 'wrongSelectorThemedComponentOverride',
|
||||
}
|
||||
|
||||
export const info = {
|
||||
name: 'themed-component-selectors',
|
||||
meta: {
|
||||
docs: {
|
||||
description: `Themeable component selectors should follow the DSpace convention
|
||||
|
||||
Each themeable component is comprised of a base component, a wrapper component and any number of themed components
|
||||
- Base components should have a selector starting with \`ds-base-\`
|
||||
- Themed components should have a selector starting with \`ds-themed-\`
|
||||
- Wrapper components should have a selector starting with \`ds-\`, but not \`ds-base-\` or \`ds-themed-\`
|
||||
- This is the regular DSpace selector prefix
|
||||
- **When making a regular component themeable, its selector prefix should be changed to \`ds-base-\`, and the new wrapper's component should reuse the previous selector**
|
||||
|
||||
Unit tests are exempt from this rule, because they may redefine components using the same class name as other themeable components elsewhere in the source.
|
||||
`,
|
||||
},
|
||||
type: 'problem',
|
||||
schema: [],
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
[Message.BASE]: 'Unthemed version of themeable component should have a selector starting with \'ds-base-\'',
|
||||
[Message.WRAPPER]: 'Themed component wrapper of themeable component shouldn\'t have a selector starting with \'ds-themed-\'',
|
||||
[Message.THEMED]: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'',
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: TSESLint.RuleContext<Message, unknown[]>) {
|
||||
const filename = getFilename(context);
|
||||
|
||||
if (filename.endsWith('.spec.ts')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
function enforceWrapperSelector(selectorNode: TSESTree.StringLiteral) {
|
||||
if (selectorNode?.value.startsWith('ds-themed-')) {
|
||||
context.report({
|
||||
messageId: Message.WRAPPER,
|
||||
node: selectorNode,
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-themed-', 'ds-')));
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function enforceBaseSelector(selectorNode: TSESTree.StringLiteral) {
|
||||
if (!selectorNode?.value.startsWith('ds-base-')) {
|
||||
context.report({
|
||||
messageId: Message.BASE,
|
||||
node: selectorNode,
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-base-')));
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function enforceThemedSelector(selectorNode: TSESTree.StringLiteral) {
|
||||
if (!selectorNode?.value.startsWith('ds-themed-')) {
|
||||
context.report({
|
||||
messageId: Message.THEMED,
|
||||
node: selectorNode,
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-themed-')));
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: TSESTree.Decorator) {
|
||||
const selectorNode = getComponentSelectorNode(node);
|
||||
|
||||
if (selectorNode === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const selector = selectorNode?.value;
|
||||
const classNode = node.parent as TSESTree.ClassDeclaration;
|
||||
const className = classNode.id?.name;
|
||||
|
||||
if (selector === undefined || className === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isThemedComponentWrapper(node)) {
|
||||
enforceWrapperSelector(selectorNode);
|
||||
} else if (inThemedComponentOverrideFile(filename)) {
|
||||
enforceThemedSelector(selectorNode);
|
||||
} else if (isThemeableComponent(className)) {
|
||||
enforceBaseSelector(selectorNode);
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const tests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'Regular non-themeable component selector',
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class Something {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Themeable component selector should replace the original version, unthemed version should be changed to ds-base-',
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-base-something',
|
||||
})
|
||||
class Something {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class ThemedSomething extends ThemedComponent<Something> {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-something',
|
||||
})
|
||||
class OverrideSomething extends Something {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Other themed component wrappers should not interfere',
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class Something {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-something-else',
|
||||
})
|
||||
class ThemedSomethingElse extends ThemedComponent<SomethingElse> {
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'Wrong selector for base component',
|
||||
filename: fixture('src/app/test/test-themeable.component.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.BASE,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-base-something',
|
||||
})
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Wrong selector for wrapper component',
|
||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-themed-something',
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRAPPER,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Wrong selector for theme override',
|
||||
filename: fixture('src/themes/test/app/test/test-themeable.component.ts'),
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-something',
|
||||
})
|
||||
class TestThememeableComponent extends BaseComponent {
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.THEMED,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-themed-something',
|
||||
})
|
||||
class TestThememeableComponent extends BaseComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default rule;
|
502
lint/src/rules/ts/themed-component-usages.ts
Normal file
502
lint/src/rules/ts/themed-component-usages.ts
Normal file
@@ -0,0 +1,502 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import {
|
||||
ESLintUtils,
|
||||
TSESLint,
|
||||
TSESTree,
|
||||
} from '@typescript-eslint/utils';
|
||||
|
||||
import { fixture } from '../../../test/fixture';
|
||||
import {
|
||||
removeWithCommas,
|
||||
replaceOrRemoveArrayIdentifier,
|
||||
} from '../../util/fix';
|
||||
import { DSpaceESLintRuleInfo } from '../../util/structure';
|
||||
import {
|
||||
allThemeableComponents,
|
||||
DISALLOWED_THEME_SELECTORS,
|
||||
fixSelectors,
|
||||
getThemeableComponentByBaseClass,
|
||||
isAllowedUnthemedUsage,
|
||||
} from '../../util/theme-support';
|
||||
import {
|
||||
findImportSpecifier,
|
||||
findUsages,
|
||||
findUsagesByName,
|
||||
getFilename,
|
||||
relativePath,
|
||||
} from '../../util/typescript';
|
||||
|
||||
export enum Message {
|
||||
WRONG_CLASS = 'mustUseThemedWrapperClass',
|
||||
WRONG_IMPORT = 'mustImportThemedWrapper',
|
||||
WRONG_SELECTOR = 'mustUseThemedWrapperSelector',
|
||||
BASE_IN_MODULE = 'baseComponentNotNeededInModule',
|
||||
}
|
||||
|
||||
export const info = {
|
||||
name: 'themed-component-usages',
|
||||
meta: {
|
||||
docs: {
|
||||
description: `Themeable components should be used via their \`ThemedComponent\` wrapper class
|
||||
|
||||
This ensures that custom themes can correctly override _all_ instances of this component.
|
||||
There are a few exceptions where the base class can still be used:
|
||||
- Class declaration expressions (otherwise we can't declare, extend or override the class in the first place)
|
||||
- Angular modules (except for routing modules)
|
||||
- Angular \`@ViewChild\` decorators
|
||||
- Type annotations
|
||||
`,
|
||||
},
|
||||
type: 'problem',
|
||||
schema: [],
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
[Message.WRONG_CLASS]: 'Themeable components should be used via their ThemedComponent wrapper',
|
||||
[Message.WRONG_IMPORT]: 'Themeable components should be used via their ThemedComponent wrapper',
|
||||
[Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper',
|
||||
[Message.BASE_IN_MODULE]: 'Base themeable components shouldn\'t be declared in modules',
|
||||
},
|
||||
},
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: TSESLint.RuleContext<Message, unknown[]>) {
|
||||
const filename = getFilename(context);
|
||||
|
||||
function handleUnthemedUsagesInTypescript(node: TSESTree.Identifier) {
|
||||
if (isAllowedUnthemedUsage(node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = getThemeableComponentByBaseClass(node.name);
|
||||
|
||||
if (entry === undefined) {
|
||||
// this should never happen
|
||||
throw new Error(`No such themeable component in registry: '${node.name}'`);
|
||||
}
|
||||
|
||||
context.report({
|
||||
messageId: Message.WRONG_CLASS,
|
||||
node: node,
|
||||
fix(fixer) {
|
||||
if (node.parent.type === TSESTree.AST_NODE_TYPES.ArrayExpression) {
|
||||
return replaceOrRemoveArrayIdentifier(context, fixer, node, entry.wrapperClass);
|
||||
} else {
|
||||
return fixer.replaceText(node, entry.wrapperClass);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleThemedSelectorQueriesInTests(node: TSESTree.Literal) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
fix(fixer){
|
||||
const newSelector = fixSelectors(node.raw);
|
||||
return fixer.replaceText(node, newSelector);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function handleUnthemedImportsInTypescript(specifierNode: TSESTree.ImportSpecifier) {
|
||||
const allUsages = findUsages(context, specifierNode.local);
|
||||
const badUsages = allUsages.filter(usage => !isAllowedUnthemedUsage(usage));
|
||||
|
||||
if (badUsages.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const importedNode = specifierNode.imported;
|
||||
const declarationNode = specifierNode.parent as TSESTree.ImportDeclaration;
|
||||
|
||||
const entry = getThemeableComponentByBaseClass(importedNode.name);
|
||||
if (entry === undefined) {
|
||||
// this should never happen
|
||||
throw new Error(`No such themeable component in registry: '${importedNode.name}'`);
|
||||
}
|
||||
|
||||
context.report({
|
||||
messageId: Message.WRONG_IMPORT,
|
||||
node: importedNode,
|
||||
fix(fixer) {
|
||||
const ops = [];
|
||||
|
||||
const wrapperImport = findImportSpecifier(context, entry.wrapperClass);
|
||||
|
||||
if (findUsagesByName(context, entry.wrapperClass).length === 0) {
|
||||
// Wrapper is not present in this file, safe to add import
|
||||
|
||||
const newImportLine = `import { ${entry.wrapperClass} } from '${relativePath(filename, entry.wrapperPath)}';`;
|
||||
|
||||
if (declarationNode.specifiers.length === 1) {
|
||||
if (allUsages.length === badUsages.length) {
|
||||
ops.push(fixer.replaceText(declarationNode, newImportLine));
|
||||
} else if (wrapperImport === undefined) {
|
||||
ops.push(fixer.insertTextAfter(declarationNode, '\n' + newImportLine));
|
||||
}
|
||||
} else {
|
||||
ops.push(...removeWithCommas(context, fixer, specifierNode));
|
||||
if (wrapperImport === undefined) {
|
||||
ops.push(fixer.insertTextAfter(declarationNode, '\n' + newImportLine));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Wrapper already present in the file, remove import instead
|
||||
|
||||
if (allUsages.length === badUsages.length) {
|
||||
if (declarationNode.specifiers.length === 1) {
|
||||
// Make sure we remove the newline as well
|
||||
ops.push(fixer.removeRange([declarationNode.range[0], declarationNode.range[1] + 1]));
|
||||
} else {
|
||||
ops.push(...removeWithCommas(context, fixer, specifierNode));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ops;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ignore tests and non-routing modules
|
||||
if (filename.endsWith('.spec.ts')) {
|
||||
return {
|
||||
[`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests,
|
||||
};
|
||||
} else if (filename.endsWith('.cy.ts')) {
|
||||
return {
|
||||
[`CallExpression[callee.object.name = "cy"][callee.property.name = "get"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests,
|
||||
};
|
||||
} else if (
|
||||
filename.match(/(?!src\/themes\/).*(?!routing).module.ts$/)
|
||||
|| filename.match(/themed-.+\.component\.ts$/)
|
||||
) {
|
||||
// do nothing
|
||||
return {};
|
||||
} else {
|
||||
return allThemeableComponents().reduce(
|
||||
(rules, entry) => {
|
||||
return {
|
||||
...rules,
|
||||
[`:not(:matches(ClassDeclaration, ImportSpecifier)) > Identifier[name = "${entry.baseClass}"]`]: handleUnthemedUsagesInTypescript,
|
||||
[`ImportSpecifier[imported.name = "${entry.baseClass}"]`]: handleUnthemedImportsInTypescript,
|
||||
};
|
||||
}, {},
|
||||
);
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
export const tests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'allow wrapper class usages',
|
||||
code: `
|
||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
||||
|
||||
const config = {
|
||||
a: ThemedTestThemeableComponent,
|
||||
b: ChipsComponent,
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'allow base class in class declaration',
|
||||
code: `
|
||||
export class TestThemeableComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'allow inheriting from base class',
|
||||
code: `
|
||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
||||
|
||||
export class ThemedAdminSidebarComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'allow base class in ViewChild',
|
||||
code: `
|
||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
||||
|
||||
export class Something {
|
||||
@ViewChild(TestThemeableComponent) test: TestThemeableComponent;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'allow wrapper selectors in test queries',
|
||||
filename: fixture('src/app/test/test.component.spec.ts'),
|
||||
code: `
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'allow wrapper selectors in cypress queries',
|
||||
filename: fixture('src/app/test/test.component.cy.ts'),
|
||||
code: `
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'disallow direct usages of base class',
|
||||
code: `
|
||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
||||
import { TestComponent } from './app/test/test.component';
|
||||
|
||||
const config = {
|
||||
a: TestThemeableComponent,
|
||||
b: TestComponent,
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_IMPORT,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_CLASS,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
||||
import { TestComponent } from './app/test/test.component';
|
||||
|
||||
const config = {
|
||||
a: ThemedTestThemeableComponent,
|
||||
b: TestComponent,
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disallow direct usages of base class, keep other imports',
|
||||
code: `
|
||||
import { Something, TestThemeableComponent } from './app/test/test-themeable.component';
|
||||
import { TestComponent } from './app/test/test.component';
|
||||
|
||||
const config = {
|
||||
a: TestThemeableComponent,
|
||||
b: TestComponent,
|
||||
c: Something,
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_IMPORT,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_CLASS,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
import { Something } from './app/test/test-themeable.component';
|
||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
||||
import { TestComponent } from './app/test/test.component';
|
||||
|
||||
const config = {
|
||||
a: ThemedTestThemeableComponent,
|
||||
b: TestComponent,
|
||||
c: Something,
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'handle array replacements correctly',
|
||||
code: `
|
||||
const DECLARATIONS = [
|
||||
Something,
|
||||
TestThemeableComponent,
|
||||
Something,
|
||||
ThemedTestThemeableComponent,
|
||||
];
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_CLASS,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
const DECLARATIONS = [
|
||||
Something,
|
||||
Something,
|
||||
ThemedTestThemeableComponent,
|
||||
];
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disallow override selector in test queries',
|
||||
filename: fixture('src/app/test/test.component.spec.ts'),
|
||||
code: `
|
||||
By.css('ds-themed-themeable');
|
||||
By.css('#test > ds-themed-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disallow base selector in test queries',
|
||||
filename: fixture('src/app/test/test.component.spec.ts'),
|
||||
code: `
|
||||
By.css('ds-base-themeable');
|
||||
By.css('#test > ds-base-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disallow override selector in cypress queries',
|
||||
filename: fixture('src/app/test/test.component.cy.ts'),
|
||||
code: `
|
||||
cy.get('ds-themed-themeable');
|
||||
cy.get('#test > ds-themed-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
cy.get('ds-themeable');
|
||||
cy.get('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disallow base selector in cypress queries',
|
||||
filename: fixture('src/app/test/test.component.cy.ts'),
|
||||
code: `
|
||||
cy.get('ds-base-themeable');
|
||||
cy.get('#test > ds-base-themeable > #nest');
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_SELECTOR,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
cy.get('ds-themeable');
|
||||
cy.get('#test > ds-themeable > #nest');
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'edge case: unable to find usage node through usage token, but import is still flagged and fixed',
|
||||
filename: fixture('src/themes/test/app/test/other-themeable.component.ts'),
|
||||
code: `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { Context } from './app/core/shared/context.model';
|
||||
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent],
|
||||
})
|
||||
export class UsageComponent {
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_IMPORT,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_CLASS,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { Context } from './app/core/shared/context.model';
|
||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [ThemedTestThemeableComponent],
|
||||
})
|
||||
export class UsageComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'edge case edge case: both are imported, only wrapper is retained',
|
||||
filename: fixture('src/themes/test/app/test/other-themeable.component.ts'),
|
||||
code: `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { Context } from './app/core/shared/context.model';
|
||||
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
|
||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent, ThemedTestThemeableComponent],
|
||||
})
|
||||
export class UsageComponent {
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.WRONG_IMPORT,
|
||||
},
|
||||
{
|
||||
messageId: Message.WRONG_CLASS,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { Context } from './app/core/shared/context.model';
|
||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [ThemedTestThemeableComponent],
|
||||
})
|
||||
export class UsageComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export default rule;
|
83
lint/src/util/angular.ts
Normal file
83
lint/src/util/angular.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
|
||||
import { getObjectPropertyNodeByName } from './typescript';
|
||||
|
||||
export function getComponentSelectorNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.StringLiteral | undefined {
|
||||
const property = getComponentInitializerNodeByName(componentDecoratorNode, 'selector');
|
||||
|
||||
if (property !== undefined) {
|
||||
// todo: support template literals as well
|
||||
if (property.type === TSESTree.AST_NODE_TYPES.Literal && typeof property.value === 'string') {
|
||||
return property as TSESTree.StringLiteral;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getComponentStandaloneNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.BooleanLiteral | undefined {
|
||||
const property = getComponentInitializerNodeByName(componentDecoratorNode, 'standalone');
|
||||
|
||||
if (property !== undefined) {
|
||||
if (property.type === TSESTree.AST_NODE_TYPES.Literal && typeof property.value === 'boolean') {
|
||||
return property as TSESTree.BooleanLiteral;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
export function getComponentImportNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.ArrayExpression | undefined {
|
||||
const property = getComponentInitializerNodeByName(componentDecoratorNode, 'imports');
|
||||
|
||||
if (property !== undefined) {
|
||||
if (property.type === TSESTree.AST_NODE_TYPES.ArrayExpression) {
|
||||
return property as TSESTree.ArrayExpression;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getComponentClassName(decoratorNode: TSESTree.Decorator): string | undefined {
|
||||
if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (decoratorNode.parent.id?.type !== TSESTree.AST_NODE_TYPES.Identifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return decoratorNode.parent.id.name;
|
||||
}
|
||||
|
||||
export function getComponentSuperClassName(decoratorNode: TSESTree.Decorator): string | undefined {
|
||||
if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (decoratorNode.parent.superClass?.type !== TSESTree.AST_NODE_TYPES.Identifier) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return decoratorNode.parent.superClass.name;
|
||||
}
|
||||
|
||||
export function getComponentInitializer(componentDecoratorNode: TSESTree.Decorator): TSESTree.ObjectExpression {
|
||||
return (componentDecoratorNode.expression as TSESTree.CallExpression).arguments[0] as TSESTree.ObjectExpression;
|
||||
}
|
||||
|
||||
export function getComponentInitializerNodeByName(componentDecoratorNode: TSESTree.Decorator, name: string): TSESTree.Node | undefined {
|
||||
const initializer = getComponentInitializer(componentDecoratorNode);
|
||||
return getObjectPropertyNodeByName(initializer, name);
|
||||
}
|
||||
|
||||
export function isPartOfViewChild(node: TSESTree.Identifier): boolean {
|
||||
return (node.parent as any)?.callee?.name === 'ViewChild';
|
||||
}
|
125
lint/src/util/fix.ts
Normal file
125
lint/src/util/fix.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import {
|
||||
RuleContext,
|
||||
RuleFix,
|
||||
RuleFixer,
|
||||
} from '@typescript-eslint/utils/ts-eslint';
|
||||
|
||||
import { getSourceCode } from './typescript';
|
||||
|
||||
|
||||
|
||||
export function appendObjectProperties(context: RuleContext<any, any>, fixer: RuleFixer, objectNode: TSESTree.ObjectExpression, properties: string[]): RuleFix {
|
||||
// todo: may not handle empty objects too well
|
||||
const lastProperty = objectNode.properties[objectNode.properties.length - 1];
|
||||
const source = getSourceCode(context);
|
||||
const nextToken = source.getTokenAfter(lastProperty);
|
||||
|
||||
// todo: newline & indentation are hardcoded for @Component({})
|
||||
// todo: we're assuming that we need trailing commas, what if we don't?
|
||||
const newPart = '\n' + properties.map(p => ` ${p},`).join('\n');
|
||||
|
||||
if (nextToken !== null && nextToken.value === ',') {
|
||||
return fixer.insertTextAfter(nextToken, newPart);
|
||||
} else {
|
||||
return fixer.insertTextAfter(lastProperty, ',' + newPart);
|
||||
}
|
||||
}
|
||||
|
||||
export function appendArrayElement(context: RuleContext<any, any>, fixer: RuleFixer, arrayNode: TSESTree.ArrayExpression, value: string): RuleFix {
|
||||
const source = getSourceCode(context);
|
||||
|
||||
if (arrayNode.elements.length === 0) {
|
||||
// This is the first element
|
||||
const openArray = source.getTokenByRangeStart(arrayNode.range[0]);
|
||||
|
||||
if (openArray == null) {
|
||||
throw new Error('Unexpected null token for opening square bracket');
|
||||
}
|
||||
|
||||
// safe to assume the list is single-line
|
||||
return fixer.insertTextAfter(openArray, `${value}`);
|
||||
} else {
|
||||
const lastElement = arrayNode.elements[arrayNode.elements.length - 1];
|
||||
|
||||
if (lastElement == null) {
|
||||
throw new Error('Unexpected null node in array');
|
||||
}
|
||||
|
||||
const nextToken = source.getTokenAfter(lastElement);
|
||||
|
||||
// todo: we don't know if the list is chopped or not, so we can't make any assumptions -- may produce output that will be flagged by other rules on the next run!
|
||||
// todo: we're assuming that we need trailing commas, what if we don't?
|
||||
if (nextToken !== null && nextToken.value === ',') {
|
||||
return fixer.insertTextAfter(nextToken, ` ${value},`);
|
||||
} else {
|
||||
return fixer.insertTextAfter(lastElement, `, ${value},`);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export function isLast(elementNode: TSESTree.Node): boolean {
|
||||
if (!elementNode.parent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let siblingNodes: (TSESTree.Node | null)[] = [null];
|
||||
if (elementNode.parent.type === TSESTree.AST_NODE_TYPES.ArrayExpression) {
|
||||
siblingNodes = elementNode.parent.elements;
|
||||
} else if (elementNode.parent.type === TSESTree.AST_NODE_TYPES.ImportDeclaration) {
|
||||
siblingNodes = elementNode.parent.specifiers;
|
||||
}
|
||||
|
||||
return elementNode === siblingNodes[siblingNodes.length - 1];
|
||||
}
|
||||
|
||||
export function removeWithCommas(context: RuleContext<any, any>, fixer: RuleFixer, elementNode: TSESTree.Node): RuleFix[] {
|
||||
const ops = [];
|
||||
|
||||
const source = getSourceCode(context);
|
||||
let nextToken = source.getTokenAfter(elementNode);
|
||||
let prevToken = source.getTokenBefore(elementNode);
|
||||
|
||||
if (nextToken !== null && prevToken !== null) {
|
||||
if (nextToken.value === ',') {
|
||||
nextToken = source.getTokenAfter(nextToken);
|
||||
if (nextToken !== null) {
|
||||
ops.push(fixer.removeRange([elementNode.range[0], nextToken.range[0]]));
|
||||
}
|
||||
}
|
||||
if (isLast(elementNode) && prevToken.value === ',') {
|
||||
prevToken = source.getTokenBefore(prevToken);
|
||||
if (prevToken !== null) {
|
||||
ops.push(fixer.removeRange([prevToken.range[1], elementNode.range[1]]));
|
||||
}
|
||||
}
|
||||
} else if (nextToken !== null) {
|
||||
ops.push(fixer.removeRange([elementNode.range[0], nextToken.range[0]]));
|
||||
}
|
||||
|
||||
return ops;
|
||||
}
|
||||
|
||||
export function replaceOrRemoveArrayIdentifier(context: RuleContext<any, any>, fixer: RuleFixer, identifierNode: TSESTree.Identifier, newValue: string): RuleFix[] {
|
||||
if (identifierNode.parent.type !== TSESTree.AST_NODE_TYPES.ArrayExpression) {
|
||||
throw new Error('Parent node is not an array expression!');
|
||||
}
|
||||
|
||||
const array = identifierNode.parent as TSESTree.ArrayExpression;
|
||||
|
||||
for (const element of array.elements) {
|
||||
if (element !== null && element.type === TSESTree.AST_NODE_TYPES.Identifier && element.name === newValue) {
|
||||
return removeWithCommas(context, fixer, identifierNode);
|
||||
}
|
||||
}
|
||||
|
||||
return [fixer.replaceText(identifierNode, newValue)];
|
||||
}
|
28
lint/src/util/misc.ts
Normal file
28
lint/src/util/misc.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
||||
export function match(rangeA: number[], rangeB: number[]) {
|
||||
return rangeA[0] === rangeB[0] && rangeA[1] === rangeB[1];
|
||||
}
|
||||
|
||||
|
||||
export function stringLiteral(value: string): string {
|
||||
return `'${value}'`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Windows-style paths into Unix-style paths
|
||||
*/
|
||||
export function toUnixStylePath(path: string): string {
|
||||
// note: we're assuming that none of the directory/file names contain '\' or '/' characters.
|
||||
// using these characters in paths is very bad practice in general, so this should be a safe assumption.
|
||||
if (path.includes('\\')) {
|
||||
return path.replace(/^[A-Z]:\\/, '/').replaceAll('\\', '/');
|
||||
}
|
||||
return path;
|
||||
}
|
57
lint/src/util/structure.ts
Normal file
57
lint/src/util/structure.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import { TSESLint } from '@typescript-eslint/utils';
|
||||
import { RuleTester } from 'eslint';
|
||||
import { EnumType } from 'typescript';
|
||||
|
||||
export type Meta = TSESLint.RuleMetaData<string>;
|
||||
export type Valid = TSESLint.ValidTestCase<unknown[]> | RuleTester.ValidTestCase;
|
||||
export type Invalid = TSESLint.InvalidTestCase<string, unknown[]> | RuleTester.InvalidTestCase;
|
||||
|
||||
export interface DSpaceESLintRuleInfo {
|
||||
name: string;
|
||||
meta: Meta,
|
||||
defaultOptions: unknown[],
|
||||
}
|
||||
|
||||
export interface NamedTests {
|
||||
plugin: string;
|
||||
valid: Valid[];
|
||||
invalid: Invalid[];
|
||||
}
|
||||
|
||||
export interface RuleExports {
|
||||
Message: EnumType,
|
||||
info: DSpaceESLintRuleInfo,
|
||||
rule: TSESLint.RuleModule<string>,
|
||||
tests: NamedTests,
|
||||
default: unknown,
|
||||
}
|
||||
|
||||
export interface PluginExports {
|
||||
name: string,
|
||||
language: string,
|
||||
rules: Record<string, unknown>,
|
||||
index: RuleExports[],
|
||||
}
|
||||
|
||||
export function bundle(
|
||||
name: string,
|
||||
language: string,
|
||||
index: RuleExports[],
|
||||
): PluginExports {
|
||||
return index.reduce((o: PluginExports, i: RuleExports) => {
|
||||
o.rules[i.info.name] = i.rule;
|
||||
return o;
|
||||
}, {
|
||||
name,
|
||||
language,
|
||||
rules: {},
|
||||
index,
|
||||
});
|
||||
}
|
5
lint/src/util/templates/index.ejs
Normal file
5
lint/src/util/templates/index.ejs
Normal file
@@ -0,0 +1,5 @@
|
||||
[DSpace ESLint plugins](../../../lint/README.md) > <%= plugin.language %> rules
|
||||
_______
|
||||
<% rules.forEach(rule => { %>
|
||||
- [`<%= plugin.name %>/<%= rule.name %>`](./rules/<%= rule.name %>.md)<% if (rule.meta?.docs?.description) {%>: <%= rule.meta.docs.description.split('\n')[0].trim() -%><% }-%>
|
||||
<% }) %>
|
48
lint/src/util/templates/rule.ejs
Normal file
48
lint/src/util/templates/rule.ejs
Normal file
@@ -0,0 +1,48 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [<%= plugin.language %> rules](../index.md) > `<%= plugin.name %>/<%= rule.name %>`
|
||||
_______
|
||||
|
||||
<%- rule.meta.docs?.description %>
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/<%- plugin.name.replace('dspace-angular-', '') %>/<%- rule.name %>.ts)
|
||||
|
||||
### Examples
|
||||
|
||||
<% if (tests.valid) {%>
|
||||
#### Valid code
|
||||
<% tests.valid.forEach(test => { %>
|
||||
##### <%= test.name !== undefined ? test.name : 'UNNAMED' %>
|
||||
<% if (test.filename) { %>
|
||||
Filename: `<%- test.filename %>`
|
||||
<% } %>
|
||||
```<%- plugin.language.toLowerCase() %>
|
||||
<%- test.code.trim() %>
|
||||
```
|
||||
<% }) %>
|
||||
<% } %>
|
||||
|
||||
<% if (tests.invalid) {%>
|
||||
#### Invalid code <%= rule.meta.fixable ? ' & automatic fixes' : '' %>
|
||||
<% tests.invalid.forEach(test => { %>
|
||||
##### <%= test.name !== undefined ? test.name : 'UNNAMED' %>
|
||||
<% if (test.filename) { %>
|
||||
Filename: `<%- test.filename %>`
|
||||
<% } %>
|
||||
```<%- plugin.language.toLowerCase() %>
|
||||
<%- test.code.trim() %>
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
<% for (const error of test.errors) { -%>
|
||||
<%- rule.meta.messages[error.messageId] %>
|
||||
<% } -%>
|
||||
```
|
||||
<% if (test.output) { %>
|
||||
Result of `yarn lint --fix`:
|
||||
```<%- plugin.language.toLowerCase() %>
|
||||
<%- test.output.trim() %>
|
||||
```
|
||||
<% } %>
|
||||
<% }) %>
|
||||
<% } %>
|
265
lint/src/util/theme-support.ts
Normal file
265
lint/src/util/theme-support.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import { readFileSync } from 'fs';
|
||||
import { basename } from 'path';
|
||||
import ts, { Identifier } from 'typescript';
|
||||
|
||||
import {
|
||||
getComponentClassName,
|
||||
isPartOfViewChild,
|
||||
} from './angular';
|
||||
import {
|
||||
isPartOfClassDeclaration,
|
||||
isPartOfTypeExpression,
|
||||
} from './typescript';
|
||||
|
||||
/**
|
||||
* Couples a themeable Component to its ThemedComponent wrapper
|
||||
*/
|
||||
export interface ThemeableComponentRegistryEntry {
|
||||
basePath: string;
|
||||
baseFileName: string,
|
||||
baseClass: string;
|
||||
|
||||
wrapperPath: string;
|
||||
wrapperFileName: string,
|
||||
wrapperClass: string;
|
||||
}
|
||||
|
||||
function isAngularComponentDecorator(node: ts.Node) {
|
||||
if (node.kind === ts.SyntaxKind.Decorator && node.parent.kind === ts.SyntaxKind.ClassDeclaration) {
|
||||
const decorator = node as ts.Decorator;
|
||||
|
||||
if (decorator.expression.kind === ts.SyntaxKind.CallExpression) {
|
||||
const method = decorator.expression as ts.CallExpression;
|
||||
|
||||
if (method.expression.kind === ts.SyntaxKind.Identifier) {
|
||||
return (method.expression as Identifier).text === 'Component';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function findImportDeclaration(source: ts.SourceFile, identifierName: string): ts.ImportDeclaration | undefined {
|
||||
return ts.forEachChild(source, (topNode: ts.Node) => {
|
||||
if (topNode.kind === ts.SyntaxKind.ImportDeclaration) {
|
||||
const importDeclaration = topNode as ts.ImportDeclaration;
|
||||
|
||||
if (importDeclaration.importClause?.namedBindings?.kind === ts.SyntaxKind.NamedImports) {
|
||||
const namedImports = importDeclaration.importClause?.namedBindings as ts.NamedImports;
|
||||
|
||||
for (const element of namedImports.elements) {
|
||||
if (element.name.text === identifierName) {
|
||||
return importDeclaration;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Listing of all themeable Components
|
||||
*/
|
||||
class ThemeableComponentRegistry {
|
||||
public readonly entries: Set<ThemeableComponentRegistryEntry>;
|
||||
public readonly byBaseClass: Map<string, ThemeableComponentRegistryEntry>;
|
||||
public readonly byWrapperClass: Map<string, ThemeableComponentRegistryEntry>;
|
||||
public readonly byBasePath: Map<string, ThemeableComponentRegistryEntry>;
|
||||
public readonly byWrapperPath: Map<string, ThemeableComponentRegistryEntry>;
|
||||
|
||||
constructor() {
|
||||
this.entries = new Set();
|
||||
this.byBaseClass = new Map();
|
||||
this.byWrapperClass = new Map();
|
||||
this.byBasePath = new Map();
|
||||
this.byWrapperPath = new Map();
|
||||
}
|
||||
|
||||
public initialize(prefix = '') {
|
||||
if (this.entries.size > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
function registerWrapper(path: string) {
|
||||
const source = getSource(path);
|
||||
|
||||
function traverse(node: ts.Node) {
|
||||
if (node.parent !== undefined && isAngularComponentDecorator(node)) {
|
||||
const classNode = node.parent as ts.ClassDeclaration;
|
||||
|
||||
if (classNode.name === undefined || classNode.heritageClauses === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapperClass = classNode.name?.escapedText as string;
|
||||
|
||||
for (const heritageClause of classNode.heritageClauses) {
|
||||
for (const type of heritageClause.types) {
|
||||
if ((type as any).expression.escapedText === 'ThemedComponent') {
|
||||
if (type.kind !== ts.SyntaxKind.ExpressionWithTypeArguments || type.typeArguments === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const firstTypeArg = type.typeArguments[0] as ts.TypeReferenceNode;
|
||||
const baseClass = (firstTypeArg.typeName as ts.Identifier)?.escapedText;
|
||||
|
||||
if (baseClass === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const importDeclaration = findImportDeclaration(source, baseClass);
|
||||
|
||||
if (importDeclaration === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const basePath = resolveLocalPath((importDeclaration.moduleSpecifier as ts.StringLiteral).text, path);
|
||||
|
||||
themeableComponents.add({
|
||||
baseClass,
|
||||
basePath: basePath.replace(new RegExp(`^${prefix}`), ''),
|
||||
baseFileName: basename(basePath).replace(/\.ts$/, ''),
|
||||
wrapperClass,
|
||||
wrapperPath: path.replace(new RegExp(`^${prefix}`), ''),
|
||||
wrapperFileName: basename(path).replace(/\.ts$/, ''),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
} else {
|
||||
ts.forEachChild(node, traverse);
|
||||
}
|
||||
}
|
||||
|
||||
traverse(source);
|
||||
}
|
||||
|
||||
const glob = require('glob');
|
||||
|
||||
// note: this outputs Unix-style paths on Windows
|
||||
const wrappers: string[] = glob.GlobSync(prefix + 'src/app/**/themed-*.component.ts', { ignore: 'node_modules/**' }).found;
|
||||
|
||||
for (const wrapper of wrappers) {
|
||||
registerWrapper(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
private add(entry: ThemeableComponentRegistryEntry) {
|
||||
this.entries.add(entry);
|
||||
this.byBaseClass.set(entry.baseClass, entry);
|
||||
this.byWrapperClass.set(entry.wrapperClass, entry);
|
||||
this.byBasePath.set(entry.basePath, entry);
|
||||
this.byWrapperPath.set(entry.wrapperPath, entry);
|
||||
}
|
||||
}
|
||||
|
||||
export const themeableComponents = new ThemeableComponentRegistry();
|
||||
|
||||
/**
|
||||
* Construct the AST of a TypeScript source file
|
||||
* @param file
|
||||
*/
|
||||
function getSource(file: string): ts.SourceFile {
|
||||
return ts.createSourceFile(
|
||||
file,
|
||||
readFileSync(file).toString(),
|
||||
ts.ScriptTarget.ES2020, // todo: actually use tsconfig.json?
|
||||
/*setParentNodes */ true,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a possibly relative local path into an absolute path starting from the root directory of the project
|
||||
*/
|
||||
function resolveLocalPath(path: string, relativeTo: string) {
|
||||
if (path.startsWith('src/')) {
|
||||
return path;
|
||||
} else if (path.startsWith('./')) {
|
||||
const parts = relativeTo.split('/');
|
||||
return [
|
||||
...parts.slice(0, parts.length - 1),
|
||||
path.replace(/^.\//, ''),
|
||||
].join('/') + '.ts';
|
||||
} else {
|
||||
throw new Error(`Unsupported local path: ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function isThemedComponentWrapper(decoratorNode: TSESTree.Decorator): boolean {
|
||||
if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (decoratorNode.parent.superClass?.type !== TSESTree.AST_NODE_TYPES.Identifier) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (decoratorNode.parent.superClass as any)?.name === 'ThemedComponent';
|
||||
}
|
||||
|
||||
export function getBaseComponentClassName(decoratorNode: TSESTree.Decorator): string | undefined {
|
||||
const wrapperClass = getComponentClassName(decoratorNode);
|
||||
|
||||
if (wrapperClass === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
themeableComponents.initialize();
|
||||
const entry = themeableComponents.byWrapperClass.get(wrapperClass);
|
||||
|
||||
if (entry === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return entry.baseClass;
|
||||
}
|
||||
|
||||
export function isThemeableComponent(className: string): boolean {
|
||||
themeableComponents.initialize();
|
||||
return themeableComponents.byBaseClass.has(className);
|
||||
}
|
||||
|
||||
export function inThemedComponentOverrideFile(filename: string): boolean {
|
||||
const match = filename.match(/src\/themes\/[^\/]+\/(app\/.*)/);
|
||||
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
themeableComponents.initialize();
|
||||
// todo: this is fragile!
|
||||
return themeableComponents.byBasePath.has(`src/${match[1]}`);
|
||||
}
|
||||
|
||||
export function allThemeableComponents(): ThemeableComponentRegistryEntry[] {
|
||||
themeableComponents.initialize();
|
||||
return [...themeableComponents.entries];
|
||||
}
|
||||
|
||||
export function getThemeableComponentByBaseClass(baseClass: string): ThemeableComponentRegistryEntry | undefined {
|
||||
themeableComponents.initialize();
|
||||
return themeableComponents.byBaseClass.get(baseClass);
|
||||
}
|
||||
|
||||
export function isAllowedUnthemedUsage(usageNode: TSESTree.Identifier) {
|
||||
return isPartOfClassDeclaration(usageNode) || isPartOfTypeExpression(usageNode) || isPartOfViewChild(usageNode);
|
||||
}
|
||||
|
||||
export const DISALLOWED_THEME_SELECTORS = 'ds-(base|themed)-';
|
||||
|
||||
export function fixSelectors(text: string): string {
|
||||
return text.replaceAll(/ds-(base|themed)-/g, 'ds-');
|
||||
}
|
154
lint/src/util/typescript.ts
Normal file
154
lint/src/util/typescript.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import {
|
||||
TSESLint,
|
||||
TSESTree,
|
||||
} from '@typescript-eslint/utils';
|
||||
|
||||
import {
|
||||
match,
|
||||
toUnixStylePath,
|
||||
} from './misc';
|
||||
|
||||
export type AnyRuleContext = TSESLint.RuleContext<string, unknown[]>;
|
||||
|
||||
/**
|
||||
* Return the current filename based on the ESLint rule context as a Unix-style path.
|
||||
* This is easier for regex and comparisons to glob paths.
|
||||
*/
|
||||
export function getFilename(context: AnyRuleContext): string {
|
||||
// TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?)
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
return toUnixStylePath(context.getFilename());
|
||||
}
|
||||
|
||||
export function getSourceCode(context: AnyRuleContext): TSESLint.SourceCode {
|
||||
// TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?)
|
||||
// eslint-disable-next-line deprecation/deprecation
|
||||
return context.getSourceCode();
|
||||
}
|
||||
|
||||
export function getObjectPropertyNodeByName(objectNode: TSESTree.ObjectExpression, propertyName: string): TSESTree.Node | undefined {
|
||||
for (const propertyNode of objectNode.properties) {
|
||||
if (
|
||||
propertyNode.type === TSESTree.AST_NODE_TYPES.Property
|
||||
&& (
|
||||
(
|
||||
propertyNode.key?.type === TSESTree.AST_NODE_TYPES.Identifier
|
||||
&& propertyNode.key?.name === propertyName
|
||||
) || (
|
||||
propertyNode.key?.type === TSESTree.AST_NODE_TYPES.Literal
|
||||
&& propertyNode.key?.value === propertyName
|
||||
)
|
||||
)
|
||||
) {
|
||||
return propertyNode.value;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function findUsages(context: AnyRuleContext, localNode: TSESTree.Identifier): TSESTree.Identifier[] {
|
||||
const source = getSourceCode(context);
|
||||
|
||||
const usages: TSESTree.Identifier[] = [];
|
||||
|
||||
for (const token of source.ast.tokens) {
|
||||
if (token.type === TSESTree.AST_TOKEN_TYPES.Identifier && token.value === localNode.name && !match(token.range, localNode.range)) {
|
||||
const node = source.getNodeByRangeIndex(token.range[0]);
|
||||
// todo: in some cases, the resulting node can actually be the whole program (!)
|
||||
if (node !== null) {
|
||||
usages.push(node as TSESTree.Identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return usages;
|
||||
}
|
||||
|
||||
export function findUsagesByName(context: AnyRuleContext, identifier: string): TSESTree.Identifier[] {
|
||||
const source = getSourceCode(context);
|
||||
|
||||
const usages: TSESTree.Identifier[] = [];
|
||||
|
||||
for (const token of source.ast.tokens) {
|
||||
if (token.type === TSESTree.AST_TOKEN_TYPES.Identifier && token.value === identifier) {
|
||||
const node = source.getNodeByRangeIndex(token.range[0]);
|
||||
// todo: in some cases, the resulting node can actually be the whole program (!)
|
||||
if (node !== null) {
|
||||
usages.push(node as TSESTree.Identifier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return usages;
|
||||
}
|
||||
|
||||
export function isPartOfTypeExpression(node: TSESTree.Identifier): boolean {
|
||||
return node.parent?.type?.valueOf().startsWith('TSType');
|
||||
}
|
||||
|
||||
export function isPartOfClassDeclaration(node: TSESTree.Identifier): boolean {
|
||||
return node.parent?.type === TSESTree.AST_NODE_TYPES.ClassDeclaration;
|
||||
}
|
||||
|
||||
function fromSrc(path: string): string {
|
||||
const m = path.match(/^.*(src\/.+)(\.(ts|json|js)?)$/);
|
||||
|
||||
if (m) {
|
||||
return m[1];
|
||||
} else {
|
||||
throw new Error(`Can't infer project-absolute TS/resource path from: ${path}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function relativePath(thisFile: string, importFile: string): string {
|
||||
const fromParts = fromSrc(thisFile).split('/');
|
||||
const toParts = fromSrc(importFile).split('/');
|
||||
|
||||
let lastCommon = 0;
|
||||
for (let i = 0; i < fromParts.length - 1; i++) {
|
||||
if (fromParts[i] === toParts[i]) {
|
||||
lastCommon++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const path = toParts.slice(lastCommon, toParts.length).join('/');
|
||||
const backtrack = fromParts.length - lastCommon - 1;
|
||||
|
||||
let prefix: string;
|
||||
if (backtrack > 0) {
|
||||
prefix = '../'.repeat(backtrack);
|
||||
} else {
|
||||
prefix = './';
|
||||
}
|
||||
|
||||
return prefix + path;
|
||||
}
|
||||
|
||||
|
||||
export function findImportSpecifier(context: AnyRuleContext, identifier: string): TSESTree.ImportSpecifier | undefined {
|
||||
const source = getSourceCode(context);
|
||||
|
||||
const usages: TSESTree.Identifier[] = [];
|
||||
|
||||
for (const token of source.ast.tokens) {
|
||||
if (token.type === TSESTree.AST_TOKEN_TYPES.Identifier && token.value === identifier) {
|
||||
const node = source.getNodeByRangeIndex(token.range[0]);
|
||||
// todo: in some cases, the resulting node can actually be the whole program (!)
|
||||
if (node && node.parent && node.parent.type === TSESTree.AST_NODE_TYPES.ImportSpecifier) {
|
||||
return node.parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
9
lint/test/fixture/README.md
Normal file
9
lint/test/fixture/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
# ESLint testing fixtures
|
||||
|
||||
The files in this directory are used for the ESLint testing environment
|
||||
- Some rules rely on registries that must be built up _before_ the rule is run
|
||||
- In order to test these registries, the fixture sources contain a few dummy components
|
||||
- The TypeScript ESLint test runner requires at least one dummy file to exist to run any tests
|
||||
- By default, [`test.ts`](./src/test.ts) is used. Note that this file is empty; it's only there for the TypeScript configuration, the actual content is injected from the `code` property in the tests.
|
||||
- To test rules that make assertions based on the path of the file, you'll need to include the `filename` property in the test configuration. Note that it must point to an existing file too!
|
||||
- The `filename` must be provided as `fixture('src/something.ts')`
|
13
lint/test/fixture/index.ts
Normal file
13
lint/test/fixture/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
||||
export const FIXTURE = 'lint/test/fixture/';
|
||||
|
||||
export function fixture(path: string): string {
|
||||
return FIXTURE + path;
|
||||
}
|
14
lint/test/fixture/src/app/test/test-routing.module.ts
Normal file
14
lint/test/fixture/src/app/test/test-routing.module.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import { ThemedTestThemeableComponent } from './themed-test-themeable.component';
|
||||
|
||||
export const ROUTES = [
|
||||
{
|
||||
component: ThemedTestThemeableComponent,
|
||||
},
|
||||
];
|
16
lint/test/fixture/src/app/test/test-themeable.component.ts
Normal file
16
lint/test/fixture/src/app/test/test-themeable.component.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-base-test-themeable',
|
||||
template: '',
|
||||
standalone: true,
|
||||
})
|
||||
export class TestThemeableComponent {
|
||||
}
|
8
lint/test/fixture/src/app/test/test.component.cy.ts
Normal file
8
lint/test/fixture/src/app/test/test.component.cy.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
8
lint/test/fixture/src/app/test/test.component.spec.ts
Normal file
8
lint/test/fixture/src/app/test/test.component.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
15
lint/test/fixture/src/app/test/test.component.ts
Normal file
15
lint/test/fixture/src/app/test/test.component.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-test',
|
||||
template: '',
|
||||
})
|
||||
export class TestComponent {
|
||||
}
|
24
lint/test/fixture/src/app/test/test.module.ts
Normal file
24
lint/test/fixture/src/app/test/test.module.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
// @ts-ignore
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { TestComponent } from './test.component';
|
||||
import { TestThemeableComponent } from './test-themeable.component';
|
||||
import { ThemedTestThemeableComponent } from './themed-test-themeable.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
TestComponent,
|
||||
TestThemeableComponent,
|
||||
ThemedTestThemeableComponent,
|
||||
],
|
||||
})
|
||||
export class TestModule {
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { ThemedComponent } from '../../../../../../src/app/shared/theme-support/themed.component';
|
||||
import { TestThemeableComponent } from './test-themeable.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-test-themeable',
|
||||
template: '',
|
||||
standalone: true,
|
||||
imports: [TestThemeableComponent],
|
||||
})
|
||||
export class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
protected getComponentName(): string {
|
||||
return '';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
protected importUnthemedComponent(): Promise<any> {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
}
|
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-test-themeable',
|
||||
template: '',
|
||||
})
|
||||
export class OtherThemeableComponent {
|
||||
|
||||
}
|
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
import { TestThemeableComponent as BaseComponent } from '../../../../app/test/test-themeable.component';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-themed-test-themeable',
|
||||
template: '',
|
||||
})
|
||||
export class TestThemeableComponent extends BaseComponent {
|
||||
|
||||
}
|
22
lint/test/fixture/src/themes/test/test.module.ts
Normal file
22
lint/test/fixture/src/themes/test/test.module.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
// @ts-ignore
|
||||
import { NgModule } from '@angular/core';
|
||||
|
||||
import { OtherThemeableComponent } from './app/test/other-themeable.component';
|
||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
TestThemeableComponent,
|
||||
OtherThemeableComponent,
|
||||
],
|
||||
})
|
||||
export class TestModule {
|
||||
|
||||
}
|
7
lint/test/fixture/tsconfig.json
Normal file
7
lint/test/fixture/tsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
13
lint/test/helpers.js
Normal file
13
lint/test/helpers.js
Normal file
@@ -0,0 +1,13 @@
|
||||
const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
|
||||
const StacktraceOption = require('jasmine-spec-reporter').StacktraceOption;
|
||||
|
||||
jasmine.getEnv().clearReporters(); // Clear default console reporter for those instead
|
||||
jasmine.getEnv().addReporter(new SpecReporter({
|
||||
spec: {
|
||||
displayErrorMessages: false,
|
||||
},
|
||||
summary: {
|
||||
displayFailed: true,
|
||||
displayStacktrace: StacktraceOption.PRETTY,
|
||||
},
|
||||
}));
|
26
lint/test/rules.spec.ts
Normal file
26
lint/test/rules.spec.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
||||
import { default as htmlPlugin } from '../src/rules/html';
|
||||
import { default as tsPlugin } from '../src/rules/ts';
|
||||
import {
|
||||
htmlRuleTester,
|
||||
tsRuleTester,
|
||||
} from './testing';
|
||||
|
||||
describe('TypeScript rules', () => {
|
||||
for (const { info, rule, tests } of tsPlugin.index) {
|
||||
tsRuleTester.run(info.name, rule, tests as any);
|
||||
}
|
||||
});
|
||||
|
||||
describe('HTML rules', () => {
|
||||
for (const { info, rule, tests } of htmlPlugin.index) {
|
||||
htmlRuleTester.run(info.name, rule, tests);
|
||||
}
|
||||
});
|
76
lint/test/structure.spec.ts
Normal file
76
lint/test/structure.spec.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
||||
import { default as html } from '../src/rules/html';
|
||||
import { default as ts } from '../src/rules/ts';
|
||||
|
||||
describe('plugin structure', () => {
|
||||
for (const pluginExports of [ts, html]) {
|
||||
const pluginName = pluginExports.name ?? 'UNNAMED PLUGIN';
|
||||
|
||||
describe(pluginName, () => {
|
||||
it('should have a name', () => {
|
||||
expect(pluginExports.name).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have rules', () => {
|
||||
expect(pluginExports.index).toBeTruthy();
|
||||
expect(pluginExports.rules).toBeTruthy();
|
||||
expect(pluginExports.index.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
for (const ruleExports of pluginExports.index) {
|
||||
const ruleName = ruleExports.info.name ?? 'UNNAMED RULE';
|
||||
|
||||
describe(ruleName, () => {
|
||||
it('should have a name', () => {
|
||||
expect(ruleExports.info.name).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should be included under the right name in the plugin', () => {
|
||||
expect(pluginExports.rules[ruleExports.info.name]).toBe(ruleExports.rule);
|
||||
});
|
||||
|
||||
it('should contain metadata', () => {
|
||||
expect(ruleExports.info).toBeTruthy();
|
||||
expect(ruleExports.info.name).toBeTruthy();
|
||||
expect(ruleExports.info.meta).toBeTruthy();
|
||||
expect(ruleExports.info.defaultOptions).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should contain messages', () => {
|
||||
expect(ruleExports.Message).toBeTruthy();
|
||||
expect(ruleExports.info.meta.messages).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('messages', () => {
|
||||
for (const member of Object.keys(ruleExports.Message)) {
|
||||
describe(member, () => {
|
||||
const id = (ruleExports.Message as any)[member];
|
||||
|
||||
it('should have a valid ID', () => {
|
||||
expect(id).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have valid metadata', () => {
|
||||
expect(ruleExports.info.meta.messages[id]).toBeTruthy();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should contain tests', () => {
|
||||
expect(ruleExports.tests).toBeTruthy();
|
||||
expect(ruleExports.tests.valid.length).toBeGreaterThan(0);
|
||||
expect(ruleExports.tests.invalid.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
53
lint/test/testing.ts
Normal file
53
lint/test/testing.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
||||
import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester';
|
||||
import { RuleTester } from 'eslint';
|
||||
|
||||
import { themeableComponents } from '../src/util/theme-support';
|
||||
import {
|
||||
FIXTURE,
|
||||
fixture,
|
||||
} from './fixture';
|
||||
|
||||
|
||||
// Register themed components from test fixture
|
||||
themeableComponents.initialize(FIXTURE);
|
||||
|
||||
TypeScriptRuleTester.itOnly = fit;
|
||||
TypeScriptRuleTester.itSkip = xit;
|
||||
|
||||
export const tsRuleTester = new TypeScriptRuleTester({
|
||||
parser: '@typescript-eslint/parser',
|
||||
defaultFilenames: {
|
||||
ts: fixture('src/test.ts'),
|
||||
tsx: 'n/a',
|
||||
},
|
||||
parserOptions: {
|
||||
project: fixture('tsconfig.json'),
|
||||
},
|
||||
});
|
||||
|
||||
class HtmlRuleTester extends RuleTester {
|
||||
run(name: string, rule: any, tests: { valid: any[], invalid: any[] }) {
|
||||
super.run(name, rule, {
|
||||
valid: tests.valid.map((test) => ({
|
||||
filename: fixture('test.html'),
|
||||
...test,
|
||||
})),
|
||||
invalid: tests.invalid.map((test) => ({
|
||||
filename: fixture('test.html'),
|
||||
...test,
|
||||
})),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const htmlRuleTester = new HtmlRuleTester({
|
||||
parser: require.resolve('@angular-eslint/template-parser'),
|
||||
});
|
24
lint/test/theme-support.spec.ts
Normal file
24
lint/test/theme-support.spec.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* 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/
|
||||
*/
|
||||
|
||||
import { themeableComponents } from '../src/util/theme-support';
|
||||
|
||||
describe('theme-support', () => {
|
||||
describe('themeable component registry', () => {
|
||||
it('should contain all themeable components from the fixture', () => {
|
||||
expect(themeableComponents.entries.size).toBe(1);
|
||||
expect(themeableComponents.byBasePath.size).toBe(1);
|
||||
expect(themeableComponents.byWrapperPath.size).toBe(1);
|
||||
expect(themeableComponents.byBaseClass.size).toBe(1);
|
||||
|
||||
expect(themeableComponents.byBaseClass.get('TestThemeableComponent')).toBeTruthy();
|
||||
expect(themeableComponents.byBasePath.get('src/app/test/test-themeable.component.ts')).toBeTruthy();
|
||||
expect(themeableComponents.byWrapperPath.get('src/app/test/themed-test-themeable.component.ts')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
27
lint/tsconfig.json
Normal file
27
lint/tsconfig.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2021",
|
||||
"lib": [
|
||||
"es2021"
|
||||
],
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"noImplicitReturns": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"outDir": "./dist",
|
||||
"sourceMap": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"types": [
|
||||
"jasmine",
|
||||
"node"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"test/fixture"
|
||||
]
|
||||
}
|
105
package.json
105
package.json
@@ -17,11 +17,16 @@
|
||||
"build:stats": "ng build --stats-json",
|
||||
"build:prod": "cross-env NODE_ENV=production yarn run build:ssr",
|
||||
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
||||
"build:lint": "rimraf 'lint/dist/**/*.js' 'lint/dist/**/*.js.map' && tsc -b lint/tsconfig.json",
|
||||
"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",
|
||||
"test:lint": "yarn build:lint && yarn test:lint:nobuild",
|
||||
"test:lint:nobuild": "jasmine --config=lint/jasmine.json",
|
||||
"lint": "yarn build:lint && yarn lint:nobuild",
|
||||
"lint:nobuild": "ng lint",
|
||||
"lint-fix": "yarn build:lint && ng lint --fix=true",
|
||||
"docs:lint": "ts-node --project ./lint/tsconfig.json ./lint/generate-docs.ts",
|
||||
"e2e": "cross-env NODE_ENV=production ng e2e",
|
||||
"clean:dev:config": "rimraf src/assets/config.json",
|
||||
"clean:coverage": "rimraf coverage",
|
||||
@@ -40,7 +45,8 @@
|
||||
"cypress:run": "cypress run",
|
||||
"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 ./"
|
||||
"check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./",
|
||||
"postinstall": "yarn build:lint || echo 'Skipped DSpace ESLint plugins.'"
|
||||
},
|
||||
"browser": {
|
||||
"fs": false,
|
||||
@@ -55,28 +61,28 @@
|
||||
"ts-node": "10.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^15.2.8",
|
||||
"@angular/cdk": "^15.2.8",
|
||||
"@angular/common": "^15.2.8",
|
||||
"@angular/compiler": "^15.2.8",
|
||||
"@angular/core": "^15.2.8",
|
||||
"@angular/forms": "^15.2.8",
|
||||
"@angular/localize": "15.2.8",
|
||||
"@angular/platform-browser": "^15.2.8",
|
||||
"@angular/platform-browser-dynamic": "^15.2.8",
|
||||
"@angular/platform-server": "^15.2.8",
|
||||
"@angular/router": "^15.2.8",
|
||||
"@angular/animations": "^17.3.4",
|
||||
"@angular/cdk": "^17.3.4",
|
||||
"@angular/common": "^17.3.4",
|
||||
"@angular/compiler": "^17.3.4",
|
||||
"@angular/core": "^17.3.4",
|
||||
"@angular/forms": "^17.3.4",
|
||||
"@angular/localize": "17.3.4",
|
||||
"@angular/platform-browser": "^17.3.4",
|
||||
"@angular/platform-browser-dynamic": "^17.3.4",
|
||||
"@angular/platform-server": "^17.3.4",
|
||||
"@angular/router": "^17.3.4",
|
||||
"@angular/ssr": "^17.3.0",
|
||||
"@babel/runtime": "7.21.0",
|
||||
"@kolkov/ngx-gallery": "^2.0.1",
|
||||
"@material-ui/core": "^4.11.0",
|
||||
"@material-ui/icons": "^4.11.3",
|
||||
"@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",
|
||||
"@ng-dynamic-forms/core": "^16.0.0",
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
|
||||
"@ngrx/effects": "^17.1.1",
|
||||
"@ngrx/router-store": "^17.1.1",
|
||||
"@ngrx/store": "^17.1.1",
|
||||
"@ngx-translate/core": "^14.0.0",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
||||
"@types/grecaptcha": "^3.0.4",
|
||||
@@ -93,8 +99,8 @@
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns-tz": "^1.3.7",
|
||||
"deepmerge": "^4.3.1",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"ejs": "^3.1.10",
|
||||
"express": "^4.19.2",
|
||||
"express-rate-limit": "^5.1.3",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"filesize": "^6.1.0",
|
||||
@@ -110,17 +116,15 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.14.1",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-mathjax3": "^4.3.2",
|
||||
"mirador": "^3.3.0",
|
||||
"mirador-dl-plugin": "^0.13.0",
|
||||
"mirador-share-plugin": "^0.11.0",
|
||||
"morgan": "^1.10.0",
|
||||
"ng-mocks": "^14.10.0",
|
||||
"ng2-file-upload": "1.4.0",
|
||||
"ng2-file-upload": "5.0.0",
|
||||
"ng2-nouislider": "^2.0.0",
|
||||
"ngx-infinite-scroll": "^15.0.0",
|
||||
"ngx-infinite-scroll": "^16.0.0",
|
||||
"ngx-pagination": "6.0.3",
|
||||
"ngx-sortablejs": "^11.1.0",
|
||||
"ngx-ui-switch": "^14.1.0",
|
||||
"nouislider": "^15.7.1",
|
||||
"pem": "1.14.7",
|
||||
@@ -128,28 +132,28 @@
|
||||
"react-copy-to-clipboard": "^5.1.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.0",
|
||||
"sanitize-html": "^2.10.0",
|
||||
"sanitize-html": "^2.12.1",
|
||||
"sortablejs": "1.15.0",
|
||||
"uuid": "^8.3.2",
|
||||
"webfontloader": "1.6.28",
|
||||
"zone.js": "~0.11.5"
|
||||
"zone.js": "~0.14.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "~15.0.0",
|
||||
"@angular-devkit/build-angular": "^15.2.6",
|
||||
"@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": "^15.2.6",
|
||||
"@angular/compiler-cli": "^15.2.8",
|
||||
"@angular/language-service": "^15.2.8",
|
||||
"@angular-builders/custom-webpack": "~17.0.1",
|
||||
"@angular-devkit/build-angular": "^17.3.0",
|
||||
"@angular-eslint/builder": "17.2.1",
|
||||
"@angular-eslint/bundled-angular-compiler": "17.2.1",
|
||||
"@angular-eslint/eslint-plugin": "17.2.1",
|
||||
"@angular-eslint/eslint-plugin-template": "17.2.1",
|
||||
"@angular-eslint/schematics": "17.2.1",
|
||||
"@angular-eslint/template-parser": "17.2.1",
|
||||
"@angular/cli": "^17.3.0",
|
||||
"@angular/compiler-cli": "^17.3.4",
|
||||
"@angular/language-service": "^17.3.4",
|
||||
"@cypress/schematic": "^1.5.0",
|
||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||
"@ngrx/store-devtools": "^15.4.0",
|
||||
"@ngtools/webpack": "^15.2.6",
|
||||
"@nguniversal/builders": "^15.2.1",
|
||||
"@ngrx/store-devtools": "^17.1.1",
|
||||
"@ngtools/webpack": "^16.2.12",
|
||||
"@types/deep-freeze": "0.1.2",
|
||||
"@types/ejs": "^3.1.2",
|
||||
"@types/express": "^4.17.17",
|
||||
@@ -158,9 +162,12 @@
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/node": "^14.14.9",
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@typescript-eslint/parser": "^5.59.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
||||
"@typescript-eslint/parser": "^7.2.0",
|
||||
"@typescript-eslint/rule-tester": "^7.2.0",
|
||||
"@typescript-eslint/utils": "^7.2.0",
|
||||
"axe-core": "^4.7.2",
|
||||
"browser-sync": "^3.0.0",
|
||||
"compression-webpack-plugin": "^9.2.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
@@ -169,12 +176,18 @@
|
||||
"deep-freeze": "0.0.1",
|
||||
"eslint": "^8.39.0",
|
||||
"eslint-plugin-deprecation": "^1.4.1",
|
||||
"eslint-plugin-dspace-angular-html": "link:./lint/dist/src/rules/html",
|
||||
"eslint-plugin-dspace-angular-ts": "link:./lint/dist/src/rules/ts",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-jsdoc": "^39.6.4",
|
||||
"eslint-plugin-import-newlines": "^1.3.1",
|
||||
"eslint-plugin-jsdoc": "^45.0.0",
|
||||
"eslint-plugin-jsonc": "^2.6.0",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-rxjs": "^5.0.3",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"express-static-gzip": "^2.1.7",
|
||||
"jasmine": "^3.8.0",
|
||||
"jasmine-core": "^3.8.0",
|
||||
"jasmine-marbles": "0.9.2",
|
||||
"karma": "^6.4.2",
|
||||
@@ -183,7 +196,7 @@
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"ngx-mask": "^13.1.7",
|
||||
"ngx-mask": "14.2.4",
|
||||
"nodemon": "^2.0.22",
|
||||
"postcss": "^8.4",
|
||||
"postcss-apply": "0.12.0",
|
||||
@@ -199,7 +212,7 @@
|
||||
"sass-loader": "^12.6.0",
|
||||
"sass-resources-loader": "^2.2.5",
|
||||
"ts-node": "^8.10.2",
|
||||
"typescript": "~4.8.4",
|
||||
"typescript": "~5.3.3",
|
||||
"webpack": "5.76.1",
|
||||
"webpack-bundle-analyzer": "^4.8.0",
|
||||
"webpack-cli": "^4.2.0",
|
||||
|
@@ -275,7 +275,9 @@ function readFileIfExists(pathToFile) {
|
||||
try {
|
||||
return fs.readFileSync(pathToFile, 'utf8');
|
||||
} catch (e) {
|
||||
console.error('Error:', e.stack);
|
||||
if (e instanceof Error) {
|
||||
console.error('Error:', e.stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user