mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-08-03 18:54:40 -05:00
Compare commits
100 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
18299dafd2 | ||
![]() |
817d09026e | ||
![]() |
d76b009390 | ||
![]() |
9effed3ce1 | ||
![]() |
c1bbfc5dcf | ||
![]() |
b6c9cfb76f | ||
![]() |
59ca7bbcf2 | ||
![]() |
52c8d5e999 | ||
![]() |
5851e7f1b7 | ||
![]() |
e05b3441de | ||
![]() |
0d6e79cb93 | ||
![]() |
76a102d901 | ||
![]() |
0880420ef6 | ||
![]() |
2351c79282 | ||
![]() |
d6016fc798 | ||
![]() |
bc17291006 | ||
![]() |
ed6cb14c4d | ||
![]() |
08de8a04b8 | ||
![]() |
5c67de8b47 | ||
![]() |
38b0408b1a | ||
![]() |
9ccad7ea86 | ||
![]() |
4a4e810a14 | ||
![]() |
76d2df3bde | ||
![]() |
c02563d894 | ||
![]() |
574ec6780b | ||
![]() |
9e0f56982b | ||
![]() |
1c66daf12b | ||
![]() |
59d683849e | ||
![]() |
9946acb1a0 | ||
![]() |
83a760644d | ||
![]() |
25ccff8640 | ||
![]() |
5c4c5a7794 | ||
![]() |
cb6af97595 | ||
![]() |
c4407dccf6 | ||
![]() |
ecdea4c3c8 | ||
![]() |
26d6f302cf | ||
![]() |
ecf10622ef | ||
![]() |
0fb553675b | ||
![]() |
2080fde4f9 | ||
![]() |
d10e67ce09 | ||
![]() |
74fe7c586b | ||
![]() |
05188aed6d | ||
![]() |
865efb7752 | ||
![]() |
4782b4da07 | ||
![]() |
4693632c7d | ||
![]() |
4c4b571a88 | ||
![]() |
328c87995b | ||
![]() |
a1d10e7d4a | ||
![]() |
77d9a7e9d3 | ||
![]() |
981b090088 | ||
![]() |
6d1c788ee0 | ||
![]() |
02de773d5b | ||
![]() |
2a240d83fd | ||
![]() |
25cdf7916d | ||
![]() |
11b5983a0d | ||
![]() |
4964987245 | ||
![]() |
ed129d6074 | ||
![]() |
37e928d869 | ||
![]() |
06def8c11e | ||
![]() |
10571676a4 | ||
![]() |
af5160237d | ||
![]() |
b86842ba73 | ||
![]() |
bfc271e743 | ||
![]() |
ee88140fdd | ||
![]() |
51249a1dce | ||
![]() |
57ec9e6b13 | ||
![]() |
1324d17d87 | ||
![]() |
26b438a888 | ||
![]() |
70f3f98363 | ||
![]() |
71e4be2d5e | ||
![]() |
5740806a28 | ||
![]() |
9b50a1b7a6 | ||
![]() |
19caad832e | ||
![]() |
dd6ae13281 | ||
![]() |
077abbe961 | ||
![]() |
3d85dc1127 | ||
![]() |
e3ea5dd13c | ||
![]() |
714b2ecd9c | ||
![]() |
883937bfd7 | ||
![]() |
0ebe08d796 | ||
![]() |
36b4fff5c7 | ||
![]() |
0684c8c388 | ||
![]() |
67744c877d | ||
![]() |
45d8c945e2 | ||
![]() |
ee19307ea2 | ||
![]() |
2c1cd25be4 | ||
![]() |
6e65558ea4 | ||
![]() |
304324ebd0 | ||
![]() |
97cd06d2ba | ||
![]() |
df948065a3 | ||
![]() |
f92126b44f | ||
![]() |
e329f6cdf1 | ||
![]() |
2c96438d61 | ||
![]() |
41a9aac75d | ||
![]() |
8768168536 | ||
![]() |
325809fbbf | ||
![]() |
3dd47a9f5b | ||
![]() |
00f16ef8f0 | ||
![]() |
5e67aae83b | ||
![]() |
ae5c603c98 |
14
.codecov.yml
14
.codecov.yml
@@ -1,3 +1,17 @@
|
||||
codecov:
|
||||
require_ci_to_pass: true
|
||||
# https://docs.codecov.com/docs/flags#recommended-automatic-flag-management
|
||||
# Require each flag to have 1 upload before notification
|
||||
flag_management:
|
||||
default_rules:
|
||||
after_n_builds: 1
|
||||
individual_flags:
|
||||
- name: backend
|
||||
paths:
|
||||
- src/
|
||||
- name: frontend
|
||||
paths:
|
||||
- src-ui/
|
||||
# https://docs.codecov.com/docs/pull-request-comments
|
||||
# codecov will only comment if coverage changes
|
||||
comment:
|
||||
|
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
7
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -20,11 +20,16 @@ NOTE: Please check only one box!
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] Other (please explain)
|
||||
- [ ] Other (please explain):
|
||||
|
||||
## Checklist:
|
||||
|
||||
<!--
|
||||
NOTE: PRs that do not address the following will not be merged, please do not skip any relevant items.
|
||||
-->
|
||||
|
||||
- [ ] I have read & agree with the [contributing guidelines](https://github.com/paperless-ngx/paperless-ngx/blob/main/CONTRIBUTING.md).
|
||||
- [ ] If applicable, I have included testing coverage for new code in this PR, for [backend](https://docs.paperless-ngx.com/development/#testing) and / or [front-end](https://docs.paperless-ngx.com/development/#testing-and-code-style) changes.
|
||||
- [ ] If applicable, I have tested my code for new features & regressions on both mobile & desktop devices, using the latest version of major browsers.
|
||||
- [ ] If applicable, I have checked that all tests pass, see [documentation](https://docs.paperless-ngx.com/development/#back-end-development).
|
||||
- [ ] I have run all `pre-commit` hooks, see [documentation](https://docs.paperless-ngx.com/development/#code-formatting-with-pre-commit-hooks).
|
||||
|
23
.github/stale.yml
vendored
23
.github/stale.yml
vendored
@@ -1,23 +0,0 @@
|
||||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 30
|
||||
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
|
||||
# Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: [cant-reproduce]
|
||||
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
|
||||
# See https://github.com/marketplace/stale for more info on the app
|
||||
# and https://github.com/probot/stale for the configuration docs
|
49
.github/workflows/ci.yml
vendored
49
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ on:
|
||||
env:
|
||||
# This is the version of pipenv all the steps will use
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PIP_ENV_VERSION: "2023.4.20"
|
||||
DEFAULT_PIP_ENV_VERSION: "2023.6.12"
|
||||
# This is the default version of Python to use in most steps
|
||||
# If changing this, change Dockerfile
|
||||
DEFAULT_PYTHON_VERSION: "3.9"
|
||||
@@ -77,6 +77,7 @@ jobs:
|
||||
with:
|
||||
name: documentation
|
||||
path: site/
|
||||
retention-days: 7
|
||||
|
||||
documentation-deploy:
|
||||
name: "Deploy Documentation"
|
||||
@@ -106,15 +107,6 @@ jobs:
|
||||
matrix:
|
||||
python-version: ['3.8', '3.9', '3.10']
|
||||
fail-fast: false
|
||||
env:
|
||||
# Enable Tika end to end testing
|
||||
TIKA_LIVE: 1
|
||||
# Enable paperless_mail testing against real server
|
||||
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
|
||||
PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }}
|
||||
PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }}
|
||||
# Enable Gotenberg end to end testing
|
||||
GOTENBERG_LIVE: 1
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
@@ -156,12 +148,18 @@ jobs:
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run pip list
|
||||
-
|
||||
name: Tests
|
||||
env:
|
||||
PAPERLESS_CI_TEST: 1
|
||||
# Enable paperless_mail testing against real server
|
||||
PAPERLESS_MAIL_TEST_HOST: ${{ secrets.TEST_MAIL_HOST }}
|
||||
PAPERLESS_MAIL_TEST_USER: ${{ secrets.TEST_MAIL_USER }}
|
||||
PAPERLESS_MAIL_TEST_PASSWD: ${{ secrets.TEST_MAIL_PASSWD }}
|
||||
run: |
|
||||
cd src/
|
||||
pipenv --python ${{ steps.setup-python.outputs.python-version }} run pytest -ra
|
||||
-
|
||||
name: Upload coverage to Codecov
|
||||
if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION && github.event_name == 'push'}}
|
||||
if: ${{ matrix.python-version == env.DEFAULT_PYTHON_VERSION }}
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
@@ -202,15 +200,36 @@ jobs:
|
||||
name: Linting checks
|
||||
run: cd src-ui && npm run lint
|
||||
-
|
||||
name: Run Playwright tests
|
||||
name: Run Jest unit tests
|
||||
run: cd src-ui && npm run test
|
||||
-
|
||||
name: Upload Jest coverage
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: jest-coverage-report
|
||||
path: src-ui/coverage
|
||||
retention-days: 7
|
||||
-
|
||||
name: Upload frontend coverage to Codecov
|
||||
if: always()
|
||||
uses: codecov/codecov-action@v3
|
||||
with:
|
||||
# not required for public repos, but intermittently fails otherwise
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
# future expansion
|
||||
flags: frontend
|
||||
-
|
||||
name: Run Playwright e2e tests
|
||||
run: cd src-ui && npx playwright test
|
||||
-
|
||||
name: Upload test results
|
||||
name: Upload Playwright test results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: playwright-report
|
||||
path: src-ui/playwright-report
|
||||
retention-days: 7
|
||||
|
||||
build-docker-image:
|
||||
name: Build Docker image for ${{ github.ref_name }}
|
||||
@@ -309,7 +328,7 @@ jobs:
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
tags: ${{ steps.docker-meta.outputs.tags }}
|
||||
labels: ${{ steps.docker-meta.outputs.labels }}
|
||||
# Get cache layers from this branch, then dev, then main
|
||||
# Get cache layers from this branch, then dev
|
||||
# This allows new branches to get at least some cache benefits, generally from dev
|
||||
cache-from: |
|
||||
type=registry,ref=ghcr.io/${{ steps.set-ghcr-repository.outputs.ghcr-repository }}/builder/cache/app:${{ github.ref_name }}
|
||||
@@ -331,6 +350,7 @@ jobs:
|
||||
with:
|
||||
name: frontend-compiled
|
||||
path: src/documents/static/frontend/
|
||||
retention-days: 7
|
||||
|
||||
build-release:
|
||||
needs:
|
||||
@@ -439,6 +459,7 @@ jobs:
|
||||
with:
|
||||
name: release
|
||||
path: dist/paperless-ngx.tar.xz
|
||||
retention-days: 7
|
||||
|
||||
publish-release:
|
||||
runs-on: ubuntu-22.04
|
||||
|
10
.github/workflows/cleanup-tags.yml
vendored
10
.github/workflows/cleanup-tags.yml
vendored
@@ -38,6 +38,7 @@ jobs:
|
||||
scheme: "branch"
|
||||
repo_name: "paperless-ngx"
|
||||
match_regex: "feature-"
|
||||
do_delete: "true"
|
||||
|
||||
cleanup-untagged-images:
|
||||
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
|
||||
@@ -51,14 +52,6 @@ jobs:
|
||||
include:
|
||||
- primary-name: "paperless-ngx"
|
||||
- primary-name: "paperless-ngx/builder/cache/app"
|
||||
- primary-name: "paperless-ngx/builder/qpdf"
|
||||
- primary-name: "paperless-ngx/builder/cache/qpdf"
|
||||
- primary-name: "paperless-ngx/builder/pikepdf"
|
||||
- primary-name: "paperless-ngx/builder/cache/pikepdf"
|
||||
- primary-name: "paperless-ngx/builder/jbig2enc"
|
||||
- primary-name: "paperless-ngx/builder/cache/jbig2enc"
|
||||
- primary-name: "paperless-ngx/builder/psycopg2"
|
||||
- primary-name: "paperless-ngx/builder/cache/psycopg2"
|
||||
# TODO: Remove the above and replace with the below
|
||||
# - primary-name: "builder/qpdf"
|
||||
# - primary-name: "builder/cache/qpdf"
|
||||
@@ -81,3 +74,4 @@ jobs:
|
||||
owner: "${{ github.repository_owner }}"
|
||||
is_org: "true"
|
||||
package_name: "${{ matrix.primary-name }}"
|
||||
do_delete: "true"
|
||||
|
6
.github/workflows/repo-maintenance.yml
vendored
6
.github/workflows/repo-maintenance.yml
vendored
@@ -19,9 +19,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/stale@v8
|
||||
with:
|
||||
days-before-stale: 30
|
||||
days-before-close: 7
|
||||
only-labels: 'cant-reproduce'
|
||||
days-before-stale: 7
|
||||
days-before-close: 14
|
||||
any-of-labels: 'cant-reproduce,not a bug'
|
||||
stale-issue-label: stale
|
||||
stale-pr-label: stale
|
||||
stale-issue-message: >
|
||||
|
@@ -37,7 +37,7 @@ repos:
|
||||
exclude: "(^Pipfile\\.lock$)"
|
||||
# Python hooks
|
||||
- repo: https://github.com/charliermarsh/ruff-pre-commit
|
||||
rev: 'v0.0.265'
|
||||
rev: 'v0.0.272'
|
||||
hooks:
|
||||
- id: ruff
|
||||
- repo: https://github.com/psf/black
|
||||
@@ -57,6 +57,6 @@ repos:
|
||||
args:
|
||||
- "--tab"
|
||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
||||
rev: "v0.9.0.2"
|
||||
rev: "v0.9.0.5"
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
|
31
Dockerfile
31
Dockerfile
@@ -1,11 +1,11 @@
|
||||
# syntax=docker/dockerfile:1.4
|
||||
# syntax=docker/dockerfile:1
|
||||
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/reference.md
|
||||
|
||||
# Stage: compile-frontend
|
||||
# Purpose: Compiles the frontend
|
||||
# Notes:
|
||||
# - Does NPM stuff with Typescript and such
|
||||
FROM --platform=$BUILDPLATFORM node:16-bullseye-slim AS compile-frontend
|
||||
FROM --platform=$BUILDPLATFORM docker.io/node:16-bookworm-slim AS compile-frontend
|
||||
|
||||
COPY ./src-ui /src/src-ui
|
||||
|
||||
@@ -21,7 +21,7 @@ RUN set -eux \
|
||||
# Comments:
|
||||
# - pipenv dependencies are not left in the final image
|
||||
# - pipenv can't touch the final image somehow
|
||||
FROM --platform=$BUILDPLATFORM python:3.9-alpine as pipenv-base
|
||||
FROM --platform=$BUILDPLATFORM docker.io/python:3.9-alpine as pipenv-base
|
||||
|
||||
WORKDIR /usr/src/pipenv
|
||||
|
||||
@@ -29,7 +29,7 @@ COPY Pipfile* ./
|
||||
|
||||
RUN set -eux \
|
||||
&& echo "Installing pipenv" \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.4.20 \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade pipenv==2023.6.12 \
|
||||
&& echo "Generating requirement.txt" \
|
||||
&& pipenv requirements > requirements.txt
|
||||
|
||||
@@ -37,7 +37,7 @@ RUN set -eux \
|
||||
# Purpose: The final image
|
||||
# Comments:
|
||||
# - Don't leave anything extra in here
|
||||
FROM python:3.9-slim-bullseye as main-app
|
||||
FROM docker.io/python:3.9-slim-bookworm as main-app
|
||||
|
||||
LABEL org.opencontainers.image.authors="paperless-ngx team <hello@paperless-ngx.com>"
|
||||
LABEL org.opencontainers.image.documentation="https://docs.paperless-ngx.com/"
|
||||
@@ -70,9 +70,9 @@ ARG RUNTIME_PACKAGES="\
|
||||
# Image processing
|
||||
liblept5 \
|
||||
liblcms2-2 \
|
||||
libtiff5 \
|
||||
libtiff6 \
|
||||
libfreetype6 \
|
||||
libwebp6 \
|
||||
libwebp7 \
|
||||
libopenjp2-7 \
|
||||
libimagequant0 \
|
||||
libraqm0 \
|
||||
@@ -98,6 +98,8 @@ ARG RUNTIME_PACKAGES="\
|
||||
libxml2 \
|
||||
libxslt1.1 \
|
||||
libgnutls30 \
|
||||
libqpdf29 \
|
||||
qpdf \
|
||||
# Mime type detection
|
||||
file \
|
||||
libmagic1 \
|
||||
@@ -181,7 +183,7 @@ ARG PSYCOPG2_VERSION=2.9.6
|
||||
RUN set -eux \
|
||||
&& echo "Getting binaries" \
|
||||
&& mkdir paperless-ngx \
|
||||
&& curl --fail --silent --show-error --output paperless-ngx.tar.gz --location https://github.com/paperless-ngx/builder/archive/3d6574e2dbaa8b8cdced864a256b0de59015f605.tar.gz \
|
||||
&& curl --fail --silent --show-error --output paperless-ngx.tar.gz --location https://github.com/paperless-ngx/builder/archive/58bb061b9b3b63009852d6d875f9a305d9ae6ac9.tar.gz \
|
||||
&& tar -xf paperless-ngx.tar.gz --directory paperless-ngx --strip-components=1 \
|
||||
&& cd paperless-ngx \
|
||||
# Setting a specific revision ensures we know what this installed
|
||||
@@ -189,9 +191,7 @@ RUN set -eux \
|
||||
&& echo "Installing jbig2enc" \
|
||||
&& cp ./jbig2enc/${JBIG2ENC_VERSION}/${TARGETARCH}${TARGETVARIANT}/jbig2 /usr/local/bin/ \
|
||||
&& cp ./jbig2enc/${JBIG2ENC_VERSION}/${TARGETARCH}${TARGETVARIANT}/libjbig2enc* /usr/local/lib/ \
|
||||
&& echo "Installing qpdf" \
|
||||
&& apt-get install --yes --no-install-recommends ./qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/libqpdf29_*.deb \
|
||||
&& apt-get install --yes --no-install-recommends ./qpdf/${QPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/qpdf_*.deb \
|
||||
&& chmod a+x /usr/local/bin/jbig2 \
|
||||
&& echo "Installing pikepdf and dependencies" \
|
||||
&& python3 -m pip install --no-cache-dir ./pikepdf/${PIKEPDF_VERSION}/${TARGETARCH}${TARGETVARIANT}/*.whl \
|
||||
&& python3 -m pip list \
|
||||
@@ -214,16 +214,17 @@ COPY --from=pipenv-base /usr/src/pipenv/requirements.txt ./
|
||||
ARG BUILD_PACKAGES="\
|
||||
build-essential \
|
||||
git \
|
||||
default-libmysqlclient-dev \
|
||||
python3-dev"
|
||||
default-libmysqlclient-dev"
|
||||
|
||||
RUN set -eux \
|
||||
# hadolint ignore=DL3042
|
||||
RUN --mount=type=cache,target=/root/.cache/pip/,id=pip-cache \
|
||||
set -eux \
|
||||
&& echo "Installing build system packages" \
|
||||
&& apt-get update \
|
||||
&& apt-get install --yes --quiet --no-install-recommends ${BUILD_PACKAGES} \
|
||||
&& python3 -m pip install --no-cache-dir --upgrade wheel \
|
||||
&& echo "Installing Python requirements" \
|
||||
&& python3 -m pip install --default-timeout=1000 --no-cache-dir --requirement requirements.txt \
|
||||
&& python3 -m pip install --default-timeout=1000 --requirement requirements.txt \
|
||||
&& echo "Installing NLTK data" \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" snowball_data \
|
||||
&& python3 -W ignore::RuntimeWarning -m nltk.downloader -d "/usr/share/nltk_data" stopwords \
|
||||
|
5
Pipfile
5
Pipfile
@@ -37,14 +37,13 @@ psycopg2 = "*"
|
||||
rapidfuzz = "*"
|
||||
redis = {extras = ["hiredis"], version = "*"}
|
||||
scikit-learn = "~=1.2"
|
||||
numpy = "*"
|
||||
whitenoise = "~=6.3"
|
||||
watchdog = "~=2.2"
|
||||
whoosh="~=2.7"
|
||||
inotifyrecursive = "~=0.3"
|
||||
ocrmypdf = "~=14.0"
|
||||
tqdm = "*"
|
||||
tika = "*"
|
||||
tika-client = "*"
|
||||
channels = "~=4.0"
|
||||
channels-redis = "*"
|
||||
uvicorn = {extras = ["standard"], version = "*"}
|
||||
@@ -67,6 +66,7 @@ scipy = "==1.8.1"
|
||||
reportlab = "==3.6.12"
|
||||
# Pin this until piwheels is building a newer version (see https://www.piwheels.org/project/cryptography/)
|
||||
cryptography = "==40.0.1"
|
||||
httpx = "*"
|
||||
|
||||
[dev-packages]
|
||||
# Linting
|
||||
@@ -78,6 +78,7 @@ factory-boy = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-django = "*"
|
||||
pytest-httpx = "*"
|
||||
pytest-env = "*"
|
||||
pytest-sugar = "*"
|
||||
pytest-xdist = "*"
|
||||
|
1080
Pipfile.lock
generated
1080
Pipfile.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:13
|
||||
image: docker.io/library/postgres:15
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
@@ -39,7 +39,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:13
|
||||
image: docker.io/library/postgres:15
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
@@ -35,7 +35,7 @@ services:
|
||||
- redisdata:/data
|
||||
|
||||
db:
|
||||
image: docker.io/library/postgres:13
|
||||
image: docker.io/library/postgres:15
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
|
@@ -80,7 +80,7 @@ django_checks() {
|
||||
|
||||
search_index() {
|
||||
|
||||
local -r index_version=5
|
||||
local -r index_version=6
|
||||
local -r index_version_file=${DATA_DIR}/.index_version
|
||||
|
||||
if [[ (! -f "${index_version_file}") || $(<"${index_version_file}") != "$index_version" ]]; then
|
||||
|
@@ -15,6 +15,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:consumer]
|
||||
command=python3 manage.py document_consumer
|
||||
@@ -25,6 +26,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:celery]
|
||||
|
||||
@@ -37,6 +39,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:celery-beat]
|
||||
|
||||
@@ -48,6 +51,7 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
||||
[program:celery-flower]
|
||||
command = /usr/local/bin/flower-conditional.sh
|
||||
@@ -58,3 +62,4 @@ stdout_logfile=/dev/stdout
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=0
|
||||
environment = HOME="/usr/src/paperless",USER="paperless"
|
||||
|
@@ -28,7 +28,7 @@ if __name__ == "__main__":
|
||||
except Exception as e:
|
||||
print(
|
||||
f"Redis ping #{attempt} failed.\n"
|
||||
f"Error: {str(e)}.\n"
|
||||
f"Error: {e!s}.\n"
|
||||
f"Waiting {RETRY_SLEEP_SECONDS}s",
|
||||
flush=True,
|
||||
)
|
||||
|
@@ -167,6 +167,16 @@ following:
|
||||
This might not actually do anything. Not every new paperless version
|
||||
comes with new database migrations.
|
||||
|
||||
### Database Upgrades
|
||||
|
||||
In general, paperless does not require a specific version of PostgreSQL or MariaDB and it is
|
||||
safe to update them to newer versions. However, you should always take a backup and follow
|
||||
the instructions from your database's documentation for how to upgrade between major versions.
|
||||
|
||||
For PostgreSQL, refer to [Upgrading a PostgreSQL Cluster](https://www.postgresql.org/docs/current/upgrading.html).
|
||||
|
||||
For MariaDB, refer to [Upgrading MariaDB](https://mariadb.com/kb/en/upgrading/)
|
||||
|
||||
## Downgrading Paperless {#downgrade-paperless}
|
||||
|
||||
Downgrades are possible. However, some updates also contain database
|
||||
|
13
docs/api.md
13
docs/api.md
@@ -288,10 +288,23 @@ with an optional `set_permissions` parameter which is of the form:
|
||||
}
|
||||
```
|
||||
|
||||
!!! note
|
||||
|
||||
Arrays should contain user or group ID numbers.
|
||||
|
||||
If this parameter is supplied the object's permissions will be overwritten,
|
||||
assuming the authenticated user has permission to do so (the user must be
|
||||
the object owner or a superuser).
|
||||
|
||||
### Retrieving full permissions
|
||||
|
||||
By default, the API will return a truncated version of object-level
|
||||
permissions, returning `user_can_change` indicating whether the current user
|
||||
can edit the object (either because they are the object owner or have permissions
|
||||
granted). You can pass the parameter `full_perms=true` to API calls to view the
|
||||
full permissions of objects in a format that mirrors the `set_permissions`
|
||||
parameter above.
|
||||
|
||||
## API Versioning
|
||||
|
||||
The REST API is versioned since Paperless-ngx 1.3.0.
|
||||
|
@@ -1,5 +1,187 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 1.16.3
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Set user and home environment through supervisord [@stumpylog](https://github.com/stumpylog) ([#3638](https://github.com/paperless-ngx/paperless-ngx/pull/3638))
|
||||
- Fix: Ignore errors when trying to copy the original file's stats [@stumpylog](https://github.com/stumpylog) ([#3652](https://github.com/paperless-ngx/paperless-ngx/pull/3652))
|
||||
- Fix: Copy default thumbnail if thumbnail generation fails [@plu](https://github.com/plu) ([#3632](https://github.com/paperless-ngx/paperless-ngx/pull/3632))
|
||||
- Fix: Set user and home environment through supervisord [@stumpylog](https://github.com/stumpylog) ([#3638](https://github.com/paperless-ngx/paperless-ngx/pull/3638))
|
||||
- Fix: Fix quick install with external database not being fully ready [@stumpylog](https://github.com/stumpylog) ([#3637](https://github.com/paperless-ngx/paperless-ngx/pull/3637))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Update default Postgres version for new installs [@stumpylog](https://github.com/stumpylog) ([#3640](https://github.com/paperless-ngx/paperless-ngx/pull/3640))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>2 changes</summary>
|
||||
|
||||
- Fix: Ignore errors when trying to copy the original file's stats [@stumpylog](https://github.com/stumpylog) ([#3652](https://github.com/paperless-ngx/paperless-ngx/pull/3652))
|
||||
- Fix: Copy default thumbnail if thumbnail generation fails [@plu](https://github.com/plu) ([#3632](https://github.com/paperless-ngx/paperless-ngx/pull/3632))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.16.2
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: Increase httpx operation timeouts to 30s [@stumpylog](https://github.com/stumpylog) ([#3627](https://github.com/paperless-ngx/paperless-ngx/pull/3627))
|
||||
- Fix: Better error handling and checking when parsing documents via Tika [@stumpylog](https://github.com/stumpylog) ([#3617](https://github.com/paperless-ngx/paperless-ngx/pull/3617))
|
||||
|
||||
### Development
|
||||
|
||||
- Development: frontend unit testing [@shamoon](https://github.com/shamoon) ([#3597](https://github.com/paperless-ngx/paperless-ngx/pull/3597))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Bumps the CI/Docker pipenv version [@stumpylog](https://github.com/stumpylog) ([#3622](https://github.com/paperless-ngx/paperless-ngx/pull/3622))
|
||||
- Chore: Set CI artifact retention days [@stumpylog](https://github.com/stumpylog) ([#3621](https://github.com/paperless-ngx/paperless-ngx/pull/3621))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>3 changes</summary>
|
||||
|
||||
- Fix: Increase httpx operation timeouts to 30s [@stumpylog](https://github.com/stumpylog) ([#3627](https://github.com/paperless-ngx/paperless-ngx/pull/3627))
|
||||
- Fix: Better error handling and checking when parsing documents via Tika [@stumpylog](https://github.com/stumpylog) ([#3617](https://github.com/paperless-ngx/paperless-ngx/pull/3617))
|
||||
- Development: frontend unit testing [@shamoon](https://github.com/shamoon) ([#3597](https://github.com/paperless-ngx/paperless-ngx/pull/3597))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.16.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: PIL ImportError on ARM devices with Docker [@stumpylog](https://github.com/stumpylog) ([#3605](https://github.com/paperless-ngx/paperless-ngx/pull/3605))
|
||||
|
||||
### Maintenance
|
||||
|
||||
- Chore: Enable the image cleanup action [@stumpylog](https://github.com/stumpylog) ([#3606](https://github.com/paperless-ngx/paperless-ngx/pull/3606))
|
||||
|
||||
## paperless-ngx 1.16.0
|
||||
|
||||
### Notable Changes
|
||||
|
||||
- Chore: Update base image to Debian bookworm [@stumpylog](https://github.com/stumpylog) ([#3469](https://github.com/paperless-ngx/paperless-ngx/pull/3469))
|
||||
|
||||
### Features
|
||||
|
||||
- Feature: Update to a simpler Tika library [@stumpylog](https://github.com/stumpylog) ([#3517](https://github.com/paperless-ngx/paperless-ngx/pull/3517))
|
||||
- Feature: Allow to filter documents by original filename and checksum [@jayme-github](https://github.com/jayme-github) ([#3485](https://github.com/paperless-ngx/paperless-ngx/pull/3485))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: return user first / last name from backend [@shamoon](https://github.com/shamoon) ([#3579](https://github.com/paperless-ngx/paperless-ngx/pull/3579))
|
||||
- Fix use of `PAPERLESS_DB_TIMEOUT` for all db types [@shamoon](https://github.com/shamoon) ([#3576](https://github.com/paperless-ngx/paperless-ngx/pull/3576))
|
||||
- Fix: handle mail rules with no filters on some imap servers [@shamoon](https://github.com/shamoon) ([#3554](https://github.com/paperless-ngx/paperless-ngx/pull/3554))
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Chore: Python dependency updates (celery 5.3.0 in particular) [@stumpylog](https://github.com/stumpylog) ([#3584](https://github.com/paperless-ngx/paperless-ngx/pull/3584))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>8 changes</summary>
|
||||
|
||||
- Chore: Python dependency updates (celery 5.3.0 in particular) [@stumpylog](https://github.com/stumpylog) ([#3584](https://github.com/paperless-ngx/paperless-ngx/pull/3584))
|
||||
- Fix: return user first / last name from backend [@shamoon](https://github.com/shamoon) ([#3579](https://github.com/paperless-ngx/paperless-ngx/pull/3579))
|
||||
- Fix use of `PAPERLESS_DB_TIMEOUT` for all db types [@shamoon](https://github.com/shamoon) ([#3576](https://github.com/paperless-ngx/paperless-ngx/pull/3576))
|
||||
- Fix: handle mail rules with no filters on some imap servers [@shamoon](https://github.com/shamoon) ([#3554](https://github.com/paperless-ngx/paperless-ngx/pull/3554))
|
||||
- Chore: Copy file stats from original file [@stumpylog](https://github.com/stumpylog) ([#3551](https://github.com/paperless-ngx/paperless-ngx/pull/3551))
|
||||
- Chore: Adds test for barcode ASN when it already exists [@stumpylog](https://github.com/stumpylog) ([#3550](https://github.com/paperless-ngx/paperless-ngx/pull/3550))
|
||||
- Feature: Update to a simpler Tika library [@stumpylog](https://github.com/stumpylog) ([#3517](https://github.com/paperless-ngx/paperless-ngx/pull/3517))
|
||||
- Feature: Allow to filter documents by original filename and checksum [@jayme-github](https://github.com/jayme-github) ([#3485](https://github.com/paperless-ngx/paperless-ngx/pull/3485))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.15.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix incorrect colors in v1.15.0 [@shamoon](https://github.com/shamoon) ([#3523](https://github.com/paperless-ngx/paperless-ngx/pull/3523))
|
||||
|
||||
### All App Changes
|
||||
|
||||
- Fix incorrect colors in v1.15.0 [@shamoon](https://github.com/shamoon) ([#3523](https://github.com/paperless-ngx/paperless-ngx/pull/3523))
|
||||
|
||||
## paperless-ngx 1.15.0
|
||||
|
||||
### Features
|
||||
|
||||
- Feature: quick filters from document detail [@shamoon](https://github.com/shamoon) ([#3476](https://github.com/paperless-ngx/paperless-ngx/pull/3476))
|
||||
- Feature: Add explanations to relative dates [@shamoon](https://github.com/shamoon) ([#3471](https://github.com/paperless-ngx/paperless-ngx/pull/3471))
|
||||
- Enhancement: paginate frontend tasks [@shamoon](https://github.com/shamoon) ([#3445](https://github.com/paperless-ngx/paperless-ngx/pull/3445))
|
||||
- Feature: Better encapsulation of barcode logic [@stumpylog](https://github.com/stumpylog) ([#3425](https://github.com/paperless-ngx/paperless-ngx/pull/3425))
|
||||
- Enhancement: Improve frontend error handling [@shamoon](https://github.com/shamoon) ([#3413](https://github.com/paperless-ngx/paperless-ngx/pull/3413))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: KeyError error on unauthenticated API calls \& persist authentication when enabled [@ajgon](https://github.com/ajgon) ([#3516](https://github.com/paperless-ngx/paperless-ngx/pull/3516))
|
||||
- Fix: exclude consumer \& AnonymousUser users from export manifest [@shamoon](https://github.com/shamoon) ([#3487](https://github.com/paperless-ngx/paperless-ngx/pull/3487))
|
||||
- Fix: prevent date suggestion search if disabled [@shamoon](https://github.com/shamoon) ([#3472](https://github.com/paperless-ngx/paperless-ngx/pull/3472))
|
||||
- Sync Pipfile.lock based on latest Pipfile [@adamantike](https://github.com/adamantike) ([#3475](https://github.com/paperless-ngx/paperless-ngx/pull/3475))
|
||||
- Fix: DocumentSerializer should return correct original filename [@jayme-github](https://github.com/jayme-github) ([#3473](https://github.com/paperless-ngx/paperless-ngx/pull/3473))
|
||||
- consumer.py: read from original file (instead of temp copy) [@chrisblech](https://github.com/chrisblech) ([#3466](https://github.com/paperless-ngx/paperless-ngx/pull/3466))
|
||||
- Bugfix: Catch an nltk AttributeError and handle it [@stumpylog](https://github.com/stumpylog) ([#3453](https://github.com/paperless-ngx/paperless-ngx/pull/3453))
|
||||
|
||||
### Documentation
|
||||
|
||||
- Adding doc on how to setup Fail2ban [@GuillaumeHullin](https://github.com/GuillaumeHullin) ([#3414](https://github.com/paperless-ngx/paperless-ngx/pull/3414))
|
||||
- Docs: Fix typo [@MarcelBochtler](https://github.com/MarcelBochtler) ([#3437](https://github.com/paperless-ngx/paperless-ngx/pull/3437))
|
||||
- [Documentation] Move nginx [@shamoon](https://github.com/shamoon) ([#3420](https://github.com/paperless-ngx/paperless-ngx/pull/3420))
|
||||
- Documentation: Note possible dependency removal for bare metal [@stumpylog](https://github.com/stumpylog) ([#3408](https://github.com/paperless-ngx/paperless-ngx/pull/3408))
|
||||
|
||||
### Development
|
||||
|
||||
- Development: migrate frontend tests to playwright [@shamoon](https://github.com/shamoon) ([#3401](https://github.com/paperless-ngx/paperless-ngx/pull/3401))
|
||||
|
||||
### Dependencies
|
||||
|
||||
<details>
|
||||
<summary>10 changes</summary>
|
||||
|
||||
- Bump eslint from 8.39.0 to 8.41.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#3513](https://github.com/paperless-ngx/paperless-ngx/pull/3513))
|
||||
- Bump concurrently from 8.0.1 to 8.1.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#3510](https://github.com/paperless-ngx/paperless-ngx/pull/3510))
|
||||
- Bump [@<!---->ng-bootstrap/ng-bootstrap from 14.1.0 to 14.2.0 in /src-ui @dependabot](https://github.com/<!---->ng-bootstrap/ng-bootstrap from 14.1.0 to 14.2.0 in /src-ui @dependabot) ([#3507](https://github.com/paperless-ngx/paperless-ngx/pull/3507))
|
||||
- Bump [@<!---->popperjs/core from 2.11.7 to 2.11.8 in /src-ui @dependabot](https://github.com/<!---->popperjs/core from 2.11.7 to 2.11.8 in /src-ui @dependabot) ([#3508](https://github.com/paperless-ngx/paperless-ngx/pull/3508))
|
||||
- Bump [@<!---->typescript-eslint/parser from 5.59.2 to 5.59.8 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/parser from 5.59.2 to 5.59.8 in /src-ui @dependabot) ([#3505](https://github.com/paperless-ngx/paperless-ngx/pull/3505))
|
||||
- Bump bootstrap from 5.2.3 to 5.3.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#3497](https://github.com/paperless-ngx/paperless-ngx/pull/3497))
|
||||
- Bump [@<!---->typescript-eslint/eslint-plugin from 5.59.2 to 5.59.8 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/eslint-plugin from 5.59.2 to 5.59.8 in /src-ui @dependabot) ([#3500](https://github.com/paperless-ngx/paperless-ngx/pull/3500))
|
||||
- Bump tslib from 2.5.0 to 2.5.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#3501](https://github.com/paperless-ngx/paperless-ngx/pull/3501))
|
||||
- Bump [@<!---->types/node from 18.16.3 to 20.2.5 in /src-ui @dependabot](https://github.com/<!---->types/node from 18.16.3 to 20.2.5 in /src-ui @dependabot) ([#3498](https://github.com/paperless-ngx/paperless-ngx/pull/3498))
|
||||
- Bump [@<!---->playwright/test from 1.33.0 to 1.34.3 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.33.0 to 1.34.3 in /src-ui @dependabot) ([#3499](https://github.com/paperless-ngx/paperless-ngx/pull/3499))
|
||||
</details>
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>22 changes</summary>
|
||||
|
||||
- Fix: KeyError error on unauthenticated API calls \& persist authentication when enabled [@ajgon](https://github.com/ajgon) ([#3516](https://github.com/paperless-ngx/paperless-ngx/pull/3516))
|
||||
- Bump eslint from 8.39.0 to 8.41.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#3513](https://github.com/paperless-ngx/paperless-ngx/pull/3513))
|
||||
- Bump concurrently from 8.0.1 to 8.1.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#3510](https://github.com/paperless-ngx/paperless-ngx/pull/3510))
|
||||
- Bump [@<!---->ng-bootstrap/ng-bootstrap from 14.1.0 to 14.2.0 in /src-ui @dependabot](https://github.com/<!---->ng-bootstrap/ng-bootstrap from 14.1.0 to 14.2.0 in /src-ui @dependabot) ([#3507](https://github.com/paperless-ngx/paperless-ngx/pull/3507))
|
||||
- Bump [@<!---->popperjs/core from 2.11.7 to 2.11.8 in /src-ui @dependabot](https://github.com/<!---->popperjs/core from 2.11.7 to 2.11.8 in /src-ui @dependabot) ([#3508](https://github.com/paperless-ngx/paperless-ngx/pull/3508))
|
||||
- Bump [@<!---->typescript-eslint/parser from 5.59.2 to 5.59.8 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/parser from 5.59.2 to 5.59.8 in /src-ui @dependabot) ([#3505](https://github.com/paperless-ngx/paperless-ngx/pull/3505))
|
||||
- Bump bootstrap from 5.2.3 to 5.3.0 in /src-ui [@dependabot](https://github.com/dependabot) ([#3497](https://github.com/paperless-ngx/paperless-ngx/pull/3497))
|
||||
- Bump [@<!---->typescript-eslint/eslint-plugin from 5.59.2 to 5.59.8 in /src-ui @dependabot](https://github.com/<!---->typescript-eslint/eslint-plugin from 5.59.2 to 5.59.8 in /src-ui @dependabot) ([#3500](https://github.com/paperless-ngx/paperless-ngx/pull/3500))
|
||||
- Bump tslib from 2.5.0 to 2.5.2 in /src-ui [@dependabot](https://github.com/dependabot) ([#3501](https://github.com/paperless-ngx/paperless-ngx/pull/3501))
|
||||
- Bump [@<!---->types/node from 18.16.3 to 20.2.5 in /src-ui @dependabot](https://github.com/<!---->types/node from 18.16.3 to 20.2.5 in /src-ui @dependabot) ([#3498](https://github.com/paperless-ngx/paperless-ngx/pull/3498))
|
||||
- Bump [@<!---->playwright/test from 1.33.0 to 1.34.3 in /src-ui @dependabot](https://github.com/<!---->playwright/test from 1.33.0 to 1.34.3 in /src-ui @dependabot) ([#3499](https://github.com/paperless-ngx/paperless-ngx/pull/3499))
|
||||
- Feature: quick filters from document detail [@shamoon](https://github.com/shamoon) ([#3476](https://github.com/paperless-ngx/paperless-ngx/pull/3476))
|
||||
- Fix: exclude consumer \& AnonymousUser users from export manifest [@shamoon](https://github.com/shamoon) ([#3487](https://github.com/paperless-ngx/paperless-ngx/pull/3487))
|
||||
- Fix: prevent date suggestion search if disabled [@shamoon](https://github.com/shamoon) ([#3472](https://github.com/paperless-ngx/paperless-ngx/pull/3472))
|
||||
- Feature: Add explanations to relative dates [@shamoon](https://github.com/shamoon) ([#3471](https://github.com/paperless-ngx/paperless-ngx/pull/3471))
|
||||
- Fix: DocumentSerializer should return correct original filename [@jayme-github](https://github.com/jayme-github) ([#3473](https://github.com/paperless-ngx/paperless-ngx/pull/3473))
|
||||
- consumer.py: read from original file (instead of temp copy) [@chrisblech](https://github.com/chrisblech) ([#3466](https://github.com/paperless-ngx/paperless-ngx/pull/3466))
|
||||
- Bugfix: Catch an nltk AttributeError and handle it [@stumpylog](https://github.com/stumpylog) ([#3453](https://github.com/paperless-ngx/paperless-ngx/pull/3453))
|
||||
- Chore: Improves the logging mixin and allows it to be typed better [@stumpylog](https://github.com/stumpylog) ([#3451](https://github.com/paperless-ngx/paperless-ngx/pull/3451))
|
||||
- Enhancement: paginate frontend tasks [@shamoon](https://github.com/shamoon) ([#3445](https://github.com/paperless-ngx/paperless-ngx/pull/3445))
|
||||
- Add SSL Support for MariaDB [@kimdre](https://github.com/kimdre) ([#3444](https://github.com/paperless-ngx/paperless-ngx/pull/3444))
|
||||
- Enhancement: Improve frontend error handling [@shamoon](https://github.com/shamoon) ([#3413](https://github.com/paperless-ngx/paperless-ngx/pull/3413))
|
||||
</details>
|
||||
|
||||
## paperless-ngx 1.14.5
|
||||
|
||||
### Features
|
||||
|
@@ -136,11 +136,11 @@ changed here.
|
||||
|
||||
Defaults to unset, using the documented path in the home directory.
|
||||
|
||||
`PAPERLESS_DB_TIMEOUT=<float>`
|
||||
`PAPERLESS_DB_TIMEOUT=<int>`
|
||||
|
||||
: Amount of time for a database connection to wait for the database to
|
||||
unlock. Mostly applicable for an sqlite based installation, consider
|
||||
changing to postgresql if you need to increase this.
|
||||
unlock. Mostly applicable for sqlite based installation. Consider changing
|
||||
to postgresql if you are having concurrency problems with sqlite.
|
||||
|
||||
Defaults to unset, keeping the Django defaults.
|
||||
|
||||
|
@@ -216,19 +216,18 @@ The front end is built using AngularJS. In order to get started, you need Node.j
|
||||
$ git ls-files -- '*.ts' | xargs pre-commit run prettier --files
|
||||
```
|
||||
|
||||
- Front end testing uses jest and cypress. There is currently a need
|
||||
for significantly more front end tests. Unit tests and e2e tests,
|
||||
- Front end testing uses Jest and Playwright. Unit tests and e2e tests,
|
||||
respectively, can be run non-interactively with:
|
||||
|
||||
```bash
|
||||
$ ng test
|
||||
$ npm run e2e:ci
|
||||
$ npx playwright test
|
||||
```
|
||||
|
||||
- Cypress also includes a UI which can be run with:
|
||||
- Playwright also includes a UI which can be run with:
|
||||
|
||||
```bash
|
||||
$ ./node_modules/.bin/cypress open
|
||||
$ npx playwright test --ui
|
||||
```
|
||||
|
||||
- In order to build the front end and serve it as part of Django, execute:
|
||||
|
@@ -27,6 +27,12 @@ system. On Linux, chances are high that this location is
|
||||
files around manually. This folder is meant to be entirely managed by
|
||||
docker and paperless.
|
||||
|
||||
!!! note
|
||||
|
||||
Files consumed from the consumption directory are re-created inside
|
||||
this media directory and are removed from the consumption directory
|
||||
itself.
|
||||
|
||||
## Let's say I want to switch tools in a year. Can I easily move to other systems?
|
||||
|
||||
**A:** Your documents are stored as plain files inside the media folder.
|
||||
|
@@ -69,7 +69,9 @@ following operations on your documents:
|
||||
No matter which options you choose, Paperless will always store the
|
||||
original document that it found in the consumption directory or in the
|
||||
mail and will never overwrite that document. Archived versions are
|
||||
stored alongside the original versions.
|
||||
stored alongside the original versions. Any files found in the
|
||||
consumption directory will stored inside the Paperless-ngx file
|
||||
structure and will not be retained in the consumption directory.
|
||||
|
||||
### The consumption directory
|
||||
|
||||
@@ -77,7 +79,9 @@ The primary method of getting documents into your database is by putting
|
||||
them in the consumption directory. The consumer waits patiently, looking
|
||||
for new additions to this directory. When it finds them,
|
||||
the consumer goes about the process of parsing them with the OCR,
|
||||
indexing what it finds, and storing it in the media directory.
|
||||
indexing what it finds, and storing it in the media directory. You should
|
||||
think of this folder as a temporary location, as files will be re-created
|
||||
inside Paperless-ngx and removed from the consumption folder.
|
||||
|
||||
Getting stuff into this directory is up to you. If you're running
|
||||
Paperless on your local computer, you might just want to drag and drop
|
||||
@@ -88,6 +92,15 @@ Typically, you're looking at an FTP server like
|
||||
[Proftpd](http://www.proftpd.org/) or a Windows folder share with
|
||||
[Samba](https://www.samba.org/).
|
||||
|
||||
!!! warning
|
||||
|
||||
Files found in the consumption directory that are consumed will be
|
||||
removed from the consumption directory and stored inside the
|
||||
Paperless-ngx file structure using any settings / storage paths
|
||||
you have specified. This action is performed as safely as possible
|
||||
but this means it is expected that files in the consumption
|
||||
directory will no longer exist (there) after being consumed.
|
||||
|
||||
### Web UI Upload
|
||||
|
||||
The dashboard has a file drop field to upload documents to paperless.
|
||||
|
@@ -384,6 +384,14 @@ fi
|
||||
|
||||
${DOCKER_COMPOSE_CMD} pull
|
||||
|
||||
if [ "$DATABASE_BACKEND" == "postgres" ] || [ "$DATABASE_BACKEND" == "mariadb" ] ; then
|
||||
echo "Starting DB first for initilzation"
|
||||
${DOCKER_COMPOSE_CMD} up --detach db
|
||||
# hopefully enough time for even the slower systems
|
||||
sleep 15
|
||||
${DOCKER_COMPOSE_CMD} stop
|
||||
fi
|
||||
|
||||
${DOCKER_COMPOSE_CMD} run --rm -e DJANGO_SUPERUSER_PASSWORD="$PASSWORD" webserver createsuperuser --noinput --username "$USERNAME" --email "$EMAIL"
|
||||
|
||||
${DOCKER_COMPOSE_CMD} up --detach
|
||||
|
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:13
|
||||
docker run -p 5432:5432 -e POSTGRES_PASSWORD=password -v paperless_pgdata:/var/lib/postgresql/data -d postgres:15
|
||||
docker run -d -p 6379:6379 redis:latest
|
||||
docker run -p 3000:3000 -d gotenberg/gotenberg:7.8 gotenberg --chromium-disable-javascript=true --chromium-allow-list="file:///tmp/.*"
|
||||
docker run -p 9998:9998 -d ghcr.io/paperless-ngx/tika:latest
|
||||
|
@@ -12,9 +12,13 @@ test('should activate / deactivate save button when changes are saved', async ({
|
||||
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
|
||||
/\w+/
|
||||
)
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Save', exact: true })
|
||||
).toBeDisabled()
|
||||
await page.getByTitle('Storage path').getByTitle('Clear all').click()
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Save', exact: true })
|
||||
).toBeEnabled()
|
||||
})
|
||||
|
||||
test('should warn on unsaved changes', async ({ page }) => {
|
||||
@@ -23,13 +27,17 @@ test('should warn on unsaved changes', async ({ page }) => {
|
||||
await expect(page.getByTitle('Correspondent', { exact: true })).toHaveText(
|
||||
/\w+/
|
||||
)
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Save', exact: true })
|
||||
).toBeDisabled()
|
||||
await page
|
||||
.getByTitle('Storage path', { exact: true })
|
||||
.getByTitle('Clear all')
|
||||
.click()
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Close' }).click()
|
||||
await expect(
|
||||
page.getByRole('button', { name: 'Save', exact: true })
|
||||
).toBeEnabled()
|
||||
await page.getByRole('button', { name: 'Close', exact: true }).click()
|
||||
await expect(page.getByRole('dialog')).toHaveText(/unsaved changes/)
|
||||
await page.getByRole('button', { name: 'Cancel' }).click()
|
||||
await page.getByRole('link', { name: 'Close all' }).click()
|
||||
|
@@ -1,8 +1,14 @@
|
||||
module.exports = {
|
||||
moduleNameMapper: {
|
||||
'@core/(.*)': '<rootDir>/src/app/core/$1',
|
||||
},
|
||||
preset: 'jest-preset-angular',
|
||||
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
|
||||
testPathIgnorePatterns: ['/node_modules/', '/cypress/'],
|
||||
testPathIgnorePatterns: [
|
||||
'/node_modules/',
|
||||
'/e2e/',
|
||||
'abstract-name-filter-service',
|
||||
'abstract-paperless-service',
|
||||
],
|
||||
transformIgnorePatterns: [`<rootDir>/node_modules/(?!.*\\.mjs$|lodash-es)`],
|
||||
moduleNameMapper: {
|
||||
'^src/(.*)': '<rootDir>/src/$1',
|
||||
},
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
40
src-ui/package-lock.json
generated
40
src-ui/package-lock.json
generated
@@ -43,7 +43,7 @@
|
||||
"@angular-eslint/template-parser": "15.2.1",
|
||||
"@angular/cli": "~15.2.7",
|
||||
"@angular/compiler-cli": "~15.2.8",
|
||||
"@playwright/test": "^1.34.3",
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^20.2.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
@@ -53,6 +53,7 @@
|
||||
"jest": "28.1.3",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jest-preset-angular": "^12.2.6",
|
||||
"jest-websocket-mock": "^2.4.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "~4.9.5",
|
||||
"wait-on": "^7.0.1"
|
||||
@@ -4228,19 +4229,19 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.34.3.tgz",
|
||||
"integrity": "sha512-zPLef6w9P6T/iT6XDYG3mvGOqOyb6eHaV9XtkunYs0+OzxBtrPAAaHotc0X+PJ00WPPnLfFBTl7mf45Mn8DBmw==",
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.35.1.tgz",
|
||||
"integrity": "sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"playwright-core": "1.34.3"
|
||||
"playwright-core": "1.35.1"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=16"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
@@ -12307,6 +12308,16 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-websocket-mock": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-websocket-mock/-/jest-websocket-mock-2.4.0.tgz",
|
||||
"integrity": "sha512-AOwyuRw6fgROXHxMOiTDl1/T4dh3fV4jDquha5N0csS/PNp742HeTZWPAuKppVRSQ8s3fUGgJHoyZT9JDO0hMA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"jest-diff": "^28.0.2",
|
||||
"mock-socket": "^9.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-worker": {
|
||||
"version": "28.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-28.1.3.tgz",
|
||||
@@ -13454,6 +13465,15 @@
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/mock-socket": {
|
||||
"version": "9.2.1",
|
||||
"resolved": "https://registry.npmjs.org/mock-socket/-/mock-socket-9.2.1.tgz",
|
||||
"integrity": "sha512-aw9F9T9G2zpGipLLhSNh6ZpgUyUl4frcVmRN08uE1NWPWg43Wx6+sGPDbQ7E5iFZZDJW5b5bypMeAEHqTbIFag==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@@ -14595,15 +14615,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.34.3",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.34.3.tgz",
|
||||
"integrity": "sha512-2pWd6G7OHKemc5x1r1rp8aQcpvDh7goMBZlJv6Co5vCNLVcQJdhxRL09SGaY6HcyHH9aT4tiynZabMofVasBYw==",
|
||||
"version": "1.35.1",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.35.1.tgz",
|
||||
"integrity": "sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
|
@@ -5,7 +5,7 @@
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"test": "ng test",
|
||||
"test": "ng test --no-watch --coverage",
|
||||
"lint": "ng lint"
|
||||
},
|
||||
"private": true,
|
||||
@@ -45,7 +45,7 @@
|
||||
"@angular-eslint/template-parser": "15.2.1",
|
||||
"@angular/cli": "~15.2.7",
|
||||
"@angular/compiler-cli": "~15.2.8",
|
||||
"@playwright/test": "^1.34.3",
|
||||
"@playwright/test": "^1.35.1",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/node": "^20.2.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.8",
|
||||
@@ -55,6 +55,7 @@
|
||||
"jest": "28.1.3",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"jest-preset-angular": "^12.2.6",
|
||||
"jest-websocket-mock": "^2.4.0",
|
||||
"ts-node": "~10.9.1",
|
||||
"typescript": "~4.9.5",
|
||||
"wait-on": "^7.0.1"
|
||||
|
@@ -1,4 +1,59 @@
|
||||
import { jest } from '@jest/globals'
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
require('jest-preset-angular/setup-jest')
|
||||
}
|
||||
import '@angular/localize/init'
|
||||
import { TextEncoder, TextDecoder } from 'util'
|
||||
global.TextEncoder = TextEncoder
|
||||
global.TextDecoder = TextDecoder
|
||||
|
||||
import { registerLocaleData } from '@angular/common'
|
||||
import localeAr from '@angular/common/locales/ar'
|
||||
import localeBe from '@angular/common/locales/be'
|
||||
import localeCa from '@angular/common/locales/ca'
|
||||
import localeCs from '@angular/common/locales/cs'
|
||||
import localeDa from '@angular/common/locales/da'
|
||||
import localeDe from '@angular/common/locales/de'
|
||||
import localeEnGb from '@angular/common/locales/en-GB'
|
||||
import localeEs from '@angular/common/locales/es'
|
||||
import localeFi from '@angular/common/locales/fi'
|
||||
import localeFr from '@angular/common/locales/fr'
|
||||
import localeIt from '@angular/common/locales/it'
|
||||
import localeLb from '@angular/common/locales/lb'
|
||||
import localeNl from '@angular/common/locales/nl'
|
||||
import localePl from '@angular/common/locales/pl'
|
||||
import localePt from '@angular/common/locales/pt'
|
||||
import localeRo from '@angular/common/locales/ro'
|
||||
import localeRu from '@angular/common/locales/ru'
|
||||
import localeSl from '@angular/common/locales/sl'
|
||||
import localeSr from '@angular/common/locales/sr'
|
||||
import localeSv from '@angular/common/locales/sv'
|
||||
import localeTr from '@angular/common/locales/tr'
|
||||
import localeZh from '@angular/common/locales/zh'
|
||||
|
||||
registerLocaleData(localeAr)
|
||||
registerLocaleData(localeBe)
|
||||
registerLocaleData(localeCa)
|
||||
registerLocaleData(localeCs)
|
||||
registerLocaleData(localeDa)
|
||||
registerLocaleData(localeDe)
|
||||
registerLocaleData(localeEnGb)
|
||||
registerLocaleData(localeEs)
|
||||
registerLocaleData(localeFi)
|
||||
registerLocaleData(localeFr)
|
||||
registerLocaleData(localeIt)
|
||||
registerLocaleData(localeLb)
|
||||
registerLocaleData(localeNl)
|
||||
registerLocaleData(localePl)
|
||||
registerLocaleData(localePt, 'pt-BR')
|
||||
registerLocaleData(localePt, 'pt-PT')
|
||||
registerLocaleData(localeRo)
|
||||
registerLocaleData(localeRu)
|
||||
registerLocaleData(localeSl)
|
||||
registerLocaleData(localeSr)
|
||||
registerLocaleData(localeSv)
|
||||
registerLocaleData(localeTr)
|
||||
registerLocaleData(localeZh)
|
||||
|
||||
/* global mocks for jsdom */
|
||||
const mock = () => {
|
||||
@@ -17,6 +72,8 @@ Object.defineProperty(window, 'getComputedStyle', {
|
||||
value: () => ['-webkit-appearance'],
|
||||
})
|
||||
|
||||
Object.defineProperty(window, 'ResizeObserver', { value: mock() })
|
||||
|
||||
Object.defineProperty(document.body.style, 'transform', {
|
||||
value: () => {
|
||||
return {
|
||||
|
@@ -22,7 +22,7 @@ import {
|
||||
PermissionType,
|
||||
} from './services/permissions.service'
|
||||
|
||||
const routes: Routes = [
|
||||
export const routes: Routes = [
|
||||
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||
{
|
||||
path: '',
|
||||
|
182
src-ui/src/app/app.component.spec.ts
Normal file
182
src-ui/src/app/app.component.spec.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
discardPeriodicTasks,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { Router } from '@angular/router'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { NgxFileDropModule } from 'ngx-file-drop'
|
||||
import { TourService, TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap'
|
||||
import { Subject } from 'rxjs'
|
||||
import { routes } from './app-routing.module'
|
||||
import { AppComponent } from './app.component'
|
||||
import { ToastsComponent } from './components/common/toasts/toasts.component'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} from './services/consumer-status.service'
|
||||
import { PermissionsService } from './services/permissions.service'
|
||||
import { ToastService, Toast } from './services/toast.service'
|
||||
import { UploadDocumentsService } from './services/upload-documents.service'
|
||||
import { SettingsService } from './services/settings.service'
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let component: AppComponent
|
||||
let fixture: ComponentFixture<AppComponent>
|
||||
let tourService: TourService
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let permissionsService: PermissionsService
|
||||
let toastService: ToastService
|
||||
let router: Router
|
||||
let settingsService: SettingsService
|
||||
let uploadDocumentsService: UploadDocumentsService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppComponent, ToastsComponent],
|
||||
providers: [],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
TourNgBootstrapModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
NgxFileDropModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
tourService = TestBed.inject(TourService)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
router = TestBed.inject(Router)
|
||||
uploadDocumentsService = TestBed.inject(UploadDocumentsService)
|
||||
fixture = TestBed.createComponent(AppComponent)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
it('should initialize the tour service & toggle class on body for styling', fakeAsync(() => {
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
fixture.detectChanges()
|
||||
const tourSpy = jest.spyOn(tourService, 'initialize')
|
||||
component.ngOnInit()
|
||||
expect(tourSpy).toHaveBeenCalled()
|
||||
tourService.start()
|
||||
expect(document.body.classList).toContain('tour-active')
|
||||
tourService.end()
|
||||
tick(500)
|
||||
expect(document.body.classList).not.toContain('tour-active')
|
||||
}))
|
||||
|
||||
it('should display toast on document consumed with link if user has access', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
let toast: Toast
|
||||
toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
expect(toast.action).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should display toast on document consumed without link if user does not have access', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
|
||||
let toast: Toast
|
||||
toastService.getToasts().subscribe((toasts) => (toast = toasts[0]))
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
expect(toast.action).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should display toast on document added', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentDetected')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should suppress dashboard notifications if set', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
||||
jest.spyOn(router, 'url', 'get').mockReturnValue('/dashboard')
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentDetected')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(toastSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should display toast on document failed', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFailed')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should disable drag-drop if on dashboard', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest.spyOn(router, 'url', 'get').mockReturnValueOnce('/dashboard')
|
||||
expect(component.dragDropEnabled).toBeFalsy()
|
||||
jest.spyOn(router, 'url', 'get').mockReturnValueOnce('/documents')
|
||||
expect(component.dragDropEnabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should enable drag-drop if user has permissions', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
expect(component.dragDropEnabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should disable drag-drop if user does not have permissions', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
|
||||
expect(component.dragDropEnabled).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support drag drop', fakeAsync(() => {
|
||||
expect(component.fileIsOver).toBeFalsy()
|
||||
component.fileOver()
|
||||
tick(1)
|
||||
fixture.detectChanges()
|
||||
expect(component.fileIsOver).toBeTruthy()
|
||||
const dropzone = fixture.debugElement.query(
|
||||
By.css('.global-dropzone-overlay')
|
||||
)
|
||||
expect(dropzone).not.toBeNull()
|
||||
component.fileLeave()
|
||||
tick(700)
|
||||
fixture.detectChanges()
|
||||
expect(dropzone.classes['hide']).toBeTruthy()
|
||||
// drop
|
||||
const toastSpy = jest.spyOn(toastService, 'show')
|
||||
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles')
|
||||
component.dropped([])
|
||||
tick(3000)
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
expect(uploadSpy).toHaveBeenCalled()
|
||||
}))
|
||||
})
|
@@ -137,6 +137,7 @@ main {
|
||||
|
||||
.sidebar .nav-link {
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover, &.active, &:focus {
|
||||
color: var(--bs-primary);
|
||||
|
272
src-ui/src/app/components/app-frame/app-frame.component.spec.ts
Normal file
272
src-ui/src/app/components/app-frame/app-frame.component.spec.ts
Normal file
@@ -0,0 +1,272 @@
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { AppFrameComponent } from './app-frame.component'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { BrowserModule } from '@angular/platform-browser'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings'
|
||||
import { RemoteVersionService } from 'src/app/services/rest/remote-version.service'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { of } from 'rxjs'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import { ActivatedRoute, Router } from '@angular/router'
|
||||
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
|
||||
import { SearchService } from 'src/app/services/rest/search.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
|
||||
const document = { id: 2, title: 'Hello world' }
|
||||
|
||||
describe('AppFrameComponent', () => {
|
||||
let component: AppFrameComponent
|
||||
let fixture: ComponentFixture<AppFrameComponent>
|
||||
let httpTestingController: HttpTestingController
|
||||
let settingsService: SettingsService
|
||||
let permissionsService: PermissionsService
|
||||
let remoteVersionService: RemoteVersionService
|
||||
let toastService: ToastService
|
||||
let openDocumentsService: OpenDocumentsService
|
||||
let searchService: SearchService
|
||||
let documentListViewService: DocumentListViewService
|
||||
let router: Router
|
||||
let savedViewSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [AppFrameComponent, IfPermissionsDirective],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
BrowserModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
NgbModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
providers: [
|
||||
SettingsService,
|
||||
SavedViewService,
|
||||
PermissionsService,
|
||||
RemoteVersionService,
|
||||
IfPermissionsDirective,
|
||||
ToastService,
|
||||
OpenDocumentsService,
|
||||
SearchService,
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
firstChild: {
|
||||
component: DocumentDetailComponent,
|
||||
},
|
||||
snapshot: {
|
||||
firstChild: {
|
||||
component: DocumentDetailComponent,
|
||||
params: {
|
||||
id: document.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
PermissionsGuard,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
const savedViewService = TestBed.inject(SavedViewService)
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
remoteVersionService = TestBed.inject(RemoteVersionService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
openDocumentsService = TestBed.inject(OpenDocumentsService)
|
||||
searchService = TestBed.inject(SearchService)
|
||||
documentListViewService = TestBed.inject(DocumentListViewService)
|
||||
router = TestBed.inject(Router)
|
||||
|
||||
jest
|
||||
.spyOn(settingsService, 'displayName', 'get')
|
||||
.mockReturnValue('Hello World')
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
|
||||
savedViewSpy = jest.spyOn(savedViewService, 'initialize')
|
||||
|
||||
fixture = TestBed.createComponent(AppFrameComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should initialize the saved view service', () => {
|
||||
expect(savedViewSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should check for update if enabled', () => {
|
||||
const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
|
||||
updateCheckSpy.mockImplementation(() => {
|
||||
return of({
|
||||
version: 'v100.0',
|
||||
update_available: true,
|
||||
})
|
||||
})
|
||||
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, true)
|
||||
component.ngOnInit()
|
||||
expect(updateCheckSpy).toHaveBeenCalled()
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).toContain('Update available')
|
||||
})
|
||||
|
||||
it('should check not for update if disabled', () => {
|
||||
const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
|
||||
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
expect(updateCheckSpy).not.toHaveBeenCalled()
|
||||
expect(fixture.nativeElement.textContent).not.toContain('Update available')
|
||||
})
|
||||
|
||||
it('should check for update if was disabled and then enabled', () => {
|
||||
const updateCheckSpy = jest.spyOn(remoteVersionService, 'checkForUpdates')
|
||||
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
|
||||
component.setUpdateChecking(true)
|
||||
fixture.detectChanges()
|
||||
expect(updateCheckSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show error on toggle update checking if store settings fails', () => {
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
settingsService.set(SETTINGS_KEYS.UPDATE_CHECKING_ENABLED, false)
|
||||
component.setUpdateChecking(true)
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||
.flush('error', {
|
||||
status: 500,
|
||||
statusText: 'error',
|
||||
})
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support toggling slim sidebar and saving', fakeAsync(() => {
|
||||
const saveSettingSpy = jest.spyOn(settingsService, 'set')
|
||||
expect(component.slimSidebarEnabled).toBeFalsy()
|
||||
expect(component.slimSidebarAnimating).toBeFalsy()
|
||||
component.toggleSlimSidebar()
|
||||
expect(component.slimSidebarAnimating).toBeTruthy()
|
||||
tick(200)
|
||||
expect(component.slimSidebarAnimating).toBeFalsy()
|
||||
expect(component.slimSidebarEnabled).toBeTruthy()
|
||||
expect(saveSettingSpy).toHaveBeenCalledWith(
|
||||
SETTINGS_KEYS.SLIM_SIDEBAR,
|
||||
true
|
||||
)
|
||||
}))
|
||||
|
||||
it('should show error on toggle slim sidebar if store settings fails', () => {
|
||||
jest.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
component.toggleSlimSidebar()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}ui_settings/`)
|
||||
.flush('error', {
|
||||
status: 500,
|
||||
statusText: 'error',
|
||||
})
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support collapsable menu', () => {
|
||||
const button: HTMLButtonElement = (
|
||||
fixture.nativeElement as HTMLDivElement
|
||||
).querySelector('button[data-toggle=collapse]')
|
||||
button.dispatchEvent(new MouseEvent('click'))
|
||||
expect(component.isMenuCollapsed).toBeFalsy()
|
||||
component.closeMenu()
|
||||
expect(component.isMenuCollapsed).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support close document & navigate on close current doc', () => {
|
||||
const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
|
||||
closeSpy.mockReturnValue(of(true))
|
||||
const routerSpy = jest.spyOn(router, 'navigate')
|
||||
component.closeDocument(document)
|
||||
expect(closeSpy).toHaveBeenCalledWith(document)
|
||||
expect(routerSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support close all documents & navigate on close current doc', () => {
|
||||
const closeAllSpy = jest.spyOn(openDocumentsService, 'closeAll')
|
||||
closeAllSpy.mockReturnValue(of(true))
|
||||
const routerSpy = jest.spyOn(router, 'navigate')
|
||||
component.closeAll()
|
||||
expect(closeAllSpy).toHaveBeenCalled()
|
||||
expect(routerSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close all documents on logout', () => {
|
||||
const closeAllSpy = jest.spyOn(openDocumentsService, 'closeAll')
|
||||
component.onLogout()
|
||||
expect(closeAllSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should warn before close if dirty documents', () => {
|
||||
jest.spyOn(openDocumentsService, 'hasDirty').mockReturnValue(true)
|
||||
expect(component.canDeactivate()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should call autocomplete endpoint on input', fakeAsync(() => {
|
||||
const autocompleteSpy = jest.spyOn(searchService, 'autocomplete')
|
||||
component.searchAutoComplete(of('hello')).subscribe()
|
||||
tick(250)
|
||||
expect(autocompleteSpy).toHaveBeenCalled()
|
||||
|
||||
component.searchAutoComplete(of('hello world 1')).subscribe()
|
||||
tick(250)
|
||||
expect(autocompleteSpy).toHaveBeenCalled()
|
||||
}))
|
||||
|
||||
it('should support reset search field', () => {
|
||||
const resetSpy = jest.spyOn(component, 'resetSearchField')
|
||||
const input = (fixture.nativeElement as HTMLDivElement).querySelector(
|
||||
'input'
|
||||
) as HTMLInputElement
|
||||
input.dispatchEvent(new KeyboardEvent('keyup', { key: 'Escape' }))
|
||||
expect(resetSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support choosing a search item', () => {
|
||||
expect(component.searchField.value).toEqual('')
|
||||
component.itemSelected({ item: 'hello', preventDefault: () => true })
|
||||
expect(component.searchField.value).toEqual('hello ')
|
||||
component.itemSelected({ item: 'world', preventDefault: () => true })
|
||||
expect(component.searchField.value).toEqual('hello world ')
|
||||
})
|
||||
|
||||
it('should navigate via quickFilter on search', () => {
|
||||
const str = 'hello world '
|
||||
component.searchField.patchValue(str)
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.search()
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
rule_type: FILTER_FULLTEXT_QUERY,
|
||||
value: str.trim(),
|
||||
},
|
||||
])
|
||||
})
|
||||
})
|
@@ -53,7 +53,7 @@ export class AppFrameComponent
|
||||
public settingsService: SettingsService,
|
||||
public tasksService: TasksService,
|
||||
private readonly toastService: ToastService,
|
||||
private permissionsService: PermissionsService
|
||||
permissionsService: PermissionsService
|
||||
) {
|
||||
super()
|
||||
|
||||
@@ -75,7 +75,7 @@ export class AppFrameComponent
|
||||
}
|
||||
|
||||
versionString = `${environment.appTitle} ${environment.version}`
|
||||
appRemoteVersion
|
||||
appRemoteVersion: AppRemoteVersion
|
||||
|
||||
isMenuCollapsed: boolean = true
|
||||
|
||||
@@ -103,7 +103,7 @@ export class AppFrameComponent
|
||||
this.toastService.showError(
|
||||
$localize`An error occurred while saving settings.`
|
||||
)
|
||||
console.log(error)
|
||||
console.warn(error)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -236,7 +236,7 @@ export class AppFrameComponent
|
||||
this.toastService.showError(
|
||||
$localize`An error occurred while saving update checking settings.`
|
||||
)
|
||||
console.log(error)
|
||||
console.warn(error)
|
||||
},
|
||||
})
|
||||
if (enable) {
|
||||
|
@@ -0,0 +1,43 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { ClearableBadgeComponent } from './clearable-badge.component'
|
||||
|
||||
describe('ClearableBadgeComponent', () => {
|
||||
let component: ClearableBadgeComponent
|
||||
let fixture: ComponentFixture<ClearableBadgeComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ClearableBadgeComponent],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ClearableBadgeComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support selected', () => {
|
||||
component.selected = true
|
||||
expect(component.active).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support numbered', () => {
|
||||
component.number = 3
|
||||
fixture.detectChanges()
|
||||
expect(component.active).toBeTruthy()
|
||||
expect((fixture.nativeElement as HTMLDivElement).textContent).toContain('3')
|
||||
})
|
||||
|
||||
it('should support selected', () => {
|
||||
let clearedResult
|
||||
component.selected = true
|
||||
fixture.detectChanges()
|
||||
component.cleared.subscribe((clear) => {
|
||||
clearedResult = clear
|
||||
})
|
||||
fixture.nativeElement
|
||||
.querySelectorAll('button')[0]
|
||||
.dispatchEvent(new MouseEvent('click'))
|
||||
expect(clearedResult).toBeTruthy()
|
||||
})
|
||||
})
|
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
discardPeriodicTasks,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { ConfirmDialogComponent } from './confirm-dialog.component'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { Subject } from 'rxjs'
|
||||
|
||||
describe('ConfirmDialogComponent', () => {
|
||||
let component: ConfirmDialogComponent
|
||||
let modal: NgbActiveModal
|
||||
let fixture: ComponentFixture<ConfirmDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ConfirmDialogComponent, SafeHtmlPipe],
|
||||
providers: [NgbActiveModal, SafeHtmlPipe],
|
||||
imports: [],
|
||||
}).compileComponents()
|
||||
|
||||
modal = TestBed.inject(NgbActiveModal)
|
||||
|
||||
fixture = TestBed.createComponent(ConfirmDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
component.title = 'Confirm delete'
|
||||
component.messageBold = 'Do you really want to delete document file.pdf?'
|
||||
component.message =
|
||||
'The files for this document will be deleted permanently. This operation cannot be undone.'
|
||||
component.btnClass = 'btn-danger'
|
||||
component.btnCaption = 'Delete document'
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support alternative', () => {
|
||||
let alternativeClickedResult
|
||||
let alternativeSubjectResult
|
||||
component.alternativeClicked.subscribe((result) => {
|
||||
alternativeClickedResult = true
|
||||
})
|
||||
component.alternative()
|
||||
// with subject
|
||||
const subject = new Subject<boolean>()
|
||||
component.alternativeSubject = subject
|
||||
subject.asObservable().subscribe((result) => {
|
||||
alternativeSubjectResult = result
|
||||
})
|
||||
component.alternative()
|
||||
expect(alternativeClickedResult).toBeTruthy()
|
||||
expect(alternativeSubjectResult).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support confirm', () => {
|
||||
let confirmClickedResult
|
||||
let confirmSubjectResult
|
||||
component.confirmClicked.subscribe((result) => {
|
||||
confirmClickedResult = true
|
||||
})
|
||||
component.confirm()
|
||||
// with subject
|
||||
const subject = new Subject<boolean>()
|
||||
component.confirmSubject = subject
|
||||
subject.asObservable().subscribe((result) => {
|
||||
confirmSubjectResult = result
|
||||
})
|
||||
component.confirm()
|
||||
expect(confirmClickedResult).toBeTruthy()
|
||||
expect(confirmSubjectResult).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support cancel & close modal', () => {
|
||||
let confirmSubjectResult
|
||||
const closeModalSpy = jest.spyOn(modal, 'close')
|
||||
component.cancel()
|
||||
const subject = new Subject<boolean>()
|
||||
component.confirmSubject = subject
|
||||
subject.asObservable().subscribe((result) => {
|
||||
confirmSubjectResult = result
|
||||
})
|
||||
component.cancel()
|
||||
// with subject
|
||||
expect(closeModalSpy).toHaveBeenCalled()
|
||||
expect(confirmSubjectResult).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support delay confirm', fakeAsync(() => {
|
||||
component.confirmButtonEnabled = false
|
||||
component.delayConfirm(1)
|
||||
expect(component.confirmButtonEnabled).toBeFalsy()
|
||||
tick(1500)
|
||||
fixture.detectChanges()
|
||||
expect(component.confirmButtonEnabled).toBeTruthy()
|
||||
discardPeriodicTasks()
|
||||
}))
|
||||
})
|
@@ -0,0 +1,141 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
let fixture: ComponentFixture<DateDropdownComponent>
|
||||
import {
|
||||
DateDropdownComponent,
|
||||
DateSelection,
|
||||
RelativeDate,
|
||||
} from './date-dropdown.component'
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DatePipe } from '@angular/common'
|
||||
|
||||
describe('DateDropdownComponent', () => {
|
||||
let component: DateDropdownComponent
|
||||
let httpTestingController: HttpTestingController
|
||||
let settingsService: SettingsService
|
||||
let settingsSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DateDropdownComponent,
|
||||
ClearableBadgeComponent,
|
||||
CustomDatePipe,
|
||||
],
|
||||
providers: [SettingsService, CustomDatePipe, DatePipe],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgbModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsSpy = jest.spyOn(settingsService, 'getLocalizedDateInputFormat')
|
||||
|
||||
fixture = TestBed.createComponent(DateDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should use a localized date placeholder', () => {
|
||||
expect(component.datePlaceHolder).toEqual('mm/dd/yyyy')
|
||||
expect(settingsSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support date input, emit change', fakeAsync(() => {
|
||||
let result: string
|
||||
component.dateAfterChange.subscribe((date) => (result = date))
|
||||
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
|
||||
input.value = '5/30/2023'
|
||||
input.dispatchEvent(new Event('change'))
|
||||
tick(500)
|
||||
expect(result).not.toBeNull()
|
||||
}))
|
||||
|
||||
it('should support date select, emit datesSet change', fakeAsync(() => {
|
||||
let result: DateSelection
|
||||
component.datesSet.subscribe((date) => (result = date))
|
||||
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
|
||||
input.value = '5/30/2023'
|
||||
input.dispatchEvent(new Event('dateSelect'))
|
||||
tick(500)
|
||||
expect(result).not.toBeNull()
|
||||
}))
|
||||
|
||||
it('should support relative dates', fakeAsync(() => {
|
||||
let result: DateSelection
|
||||
component.datesSet.subscribe((date) => (result = date))
|
||||
component.setRelativeDate(null)
|
||||
component.setRelativeDate(RelativeDate.LAST_7_DAYS)
|
||||
tick(500)
|
||||
expect(result).toEqual({
|
||||
after: null,
|
||||
before: null,
|
||||
relativeDateID: RelativeDate.LAST_7_DAYS,
|
||||
})
|
||||
}))
|
||||
|
||||
it('should support report if active', () => {
|
||||
component.relativeDate = RelativeDate.LAST_7_DAYS
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.relativeDate = null
|
||||
component.dateAfter = '2023-05-30'
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.dateAfter = null
|
||||
component.dateBefore = '2023-05-30'
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.dateBefore = null
|
||||
expect(component.isActive).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
component.dateAfter = '2023-05-30'
|
||||
component.reset()
|
||||
expect(component.dateAfter).toBeNull()
|
||||
})
|
||||
|
||||
it('should support clearAfter', () => {
|
||||
component.dateAfter = '2023-05-30'
|
||||
component.clearAfter()
|
||||
expect(component.dateAfter).toBeNull()
|
||||
})
|
||||
|
||||
it('should support clearBefore', () => {
|
||||
component.dateBefore = '2023-05-30'
|
||||
component.clearBefore()
|
||||
expect(component.dateBefore).toBeNull()
|
||||
})
|
||||
|
||||
it('should limit keyboard events', () => {
|
||||
const input: HTMLInputElement = fixture.nativeElement.querySelector('input')
|
||||
let event: KeyboardEvent = new KeyboardEvent('keypress', {
|
||||
key: '9',
|
||||
})
|
||||
let eventSpy = jest.spyOn(event, 'preventDefault')
|
||||
input.dispatchEvent(event)
|
||||
expect(eventSpy).not.toHaveBeenCalled()
|
||||
|
||||
event = new KeyboardEvent('keypress', {
|
||||
key: '{',
|
||||
})
|
||||
eventSpy = jest.spyOn(event, 'preventDefault')
|
||||
input.dispatchEvent(event)
|
||||
expect(eventSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -1,4 +1,3 @@
|
||||
import { formatDate } from '@angular/common'
|
||||
import {
|
||||
Component,
|
||||
EventEmitter,
|
||||
|
@@ -0,0 +1,55 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { CorrespondentEditDialogComponent } from './correspondent-edit-dialog.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
|
||||
describe('CorrespondentEditDialogComponent', () => {
|
||||
let component: CorrespondentEditDialogComponent
|
||||
let fixture: ComponentFixture<CorrespondentEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
CorrespondentEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(CorrespondentEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,55 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { DocumentTypeEditDialogComponent } from './document-type-edit-dialog.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
|
||||
describe('DocumentTypeEditDialogComponent', () => {
|
||||
let component: DocumentTypeEditDialogComponent
|
||||
let fixture: ComponentFixture<DocumentTypeEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DocumentTypeEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(DocumentTypeEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,234 @@
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { Component } from '@angular/core'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import {
|
||||
FormGroup,
|
||||
FormControl,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { EditDialogComponent, EditDialogMode } from './edit-dialog.component'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
MATCH_AUTO,
|
||||
MATCH_NONE,
|
||||
} from 'src/app/data/matching-model'
|
||||
import { of } from 'rxjs'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div>
|
||||
<h4 class="modal-title" id="modal-basic-title">{{ getTitle() }}</h4>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class TestComponent extends EditDialogComponent<PaperlessTag> {
|
||||
constructor(
|
||||
service: TagService,
|
||||
activeModal: NgbActiveModal,
|
||||
userService: UserService,
|
||||
settingsService: SettingsService
|
||||
) {
|
||||
super(service, activeModal, userService, settingsService)
|
||||
}
|
||||
|
||||
getForm(): FormGroup<any> {
|
||||
return new FormGroup({
|
||||
name: new FormControl(''),
|
||||
color: new FormControl(''),
|
||||
is_inbox_tag: new FormControl(false),
|
||||
permissions_form: new FormControl(null),
|
||||
matching_algorithm: new FormControl(DEFAULT_MATCHING_ALGORITHM),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const currentUser = {
|
||||
id: 99,
|
||||
username: 'user99',
|
||||
}
|
||||
|
||||
const permissions = {
|
||||
view: {
|
||||
users: [11],
|
||||
groups: [],
|
||||
},
|
||||
change: {
|
||||
users: [],
|
||||
groups: [2],
|
||||
},
|
||||
}
|
||||
|
||||
const tag = {
|
||||
id: 1,
|
||||
name: 'Tag 1',
|
||||
color: '#fff000',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: MATCH_AUTO,
|
||||
owner: 10,
|
||||
permissions,
|
||||
}
|
||||
|
||||
describe('EditDialogComponent', () => {
|
||||
let component: TestComponent
|
||||
let fixture: ComponentFixture<TestComponent>
|
||||
let tagService: TagService
|
||||
let activeModal: NgbActiveModal
|
||||
let httpTestingController: HttpTestingController
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComponent],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 13,
|
||||
username: 'user1',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SettingsService,
|
||||
useValue: {
|
||||
currentUser,
|
||||
},
|
||||
},
|
||||
TagService,
|
||||
],
|
||||
imports: [HttpClientTestingModule, FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
tagService = TestBed.inject(TagService)
|
||||
activeModal = TestBed.inject(NgbActiveModal)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should interpolate object permissions', () => {
|
||||
component.object = tag
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
component.ngOnInit()
|
||||
|
||||
expect(component.objectForm.get('permissions_form').value).toEqual({
|
||||
owner: tag.owner,
|
||||
set_permissions: permissions,
|
||||
})
|
||||
})
|
||||
|
||||
it('should delay close enabled', fakeAsync(() => {
|
||||
expect(component.closeEnabled).toBeFalsy()
|
||||
component.ngOnInit()
|
||||
tick(100)
|
||||
expect(component.closeEnabled).toBeTruthy()
|
||||
}))
|
||||
|
||||
it('should set default owner when in create mode', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
component.ngOnInit()
|
||||
expect(component.objectForm.get('permissions_form').value.owner).toEqual(
|
||||
currentUser.id
|
||||
)
|
||||
// cover optional chaining
|
||||
component.objectForm.removeControl('permissions_form')
|
||||
component.ngOnInit()
|
||||
})
|
||||
|
||||
it('should detect if pattern required', () => {
|
||||
expect(component.patternRequired).toBeFalsy()
|
||||
component.objectForm.get('matching_algorithm').setValue(MATCH_AUTO)
|
||||
expect(component.patternRequired).toBeFalsy()
|
||||
component.objectForm.get('matching_algorithm').setValue(MATCH_NONE)
|
||||
expect(component.patternRequired).toBeFalsy()
|
||||
component.objectForm.get('matching_algorithm').setValue(MATCH_ALL)
|
||||
expect(component.patternRequired).toBeTruthy()
|
||||
// coverage
|
||||
component.objectForm = null
|
||||
expect(component.patternRequired).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
// coverage
|
||||
component.dialogMode = null
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should close on cancel', () => {
|
||||
const closeSpy = jest.spyOn(activeModal, 'close')
|
||||
component.cancel()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should update an object on save in edit mode', () => {
|
||||
const updateSpy = jest.spyOn(tagService, 'update')
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create an object on save in edit mode', () => {
|
||||
const createSpy = jest.spyOn(tagService, 'create')
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
component.save()
|
||||
expect(createSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should close on successful save', () => {
|
||||
const closeSpy = jest.spyOn(activeModal, 'close')
|
||||
const successSpy = jest.spyOn(component.succeeded, 'emit')
|
||||
component.save()
|
||||
httpTestingController.expectOne(`${environment.apiBaseUrl}tags/`).flush({})
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
expect(successSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not close on failed save', () => {
|
||||
const closeSpy = jest.spyOn(activeModal, 'close')
|
||||
const failedSpy = jest.spyOn(component.failed, 'next')
|
||||
component.save()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}tags/`)
|
||||
.flush('error', {
|
||||
status: 500,
|
||||
statusText: 'error',
|
||||
})
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
expect(failedSpy).toHaveBeenCalled()
|
||||
expect(component.error).toEqual('error')
|
||||
})
|
||||
})
|
@@ -15,6 +15,11 @@ import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { PermissionsFormObject } from '../input/permissions/permissions-form/permissions-form.component'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
|
||||
export enum EditDialogMode {
|
||||
CREATE = 0,
|
||||
EDIT = 1,
|
||||
}
|
||||
|
||||
@Directive()
|
||||
export abstract class EditDialogComponent<
|
||||
T extends ObjectWithPermissions | ObjectWithId
|
||||
@@ -30,7 +35,7 @@ export abstract class EditDialogComponent<
|
||||
users: PaperlessUser[]
|
||||
|
||||
@Input()
|
||||
dialogMode: string = 'create'
|
||||
dialogMode: EditDialogMode = EditDialogMode.CREATE
|
||||
|
||||
@Input()
|
||||
object: T
|
||||
@@ -71,7 +76,7 @@ export abstract class EditDialogComponent<
|
||||
|
||||
this.userService.listAll().subscribe((r) => {
|
||||
this.users = r.results
|
||||
if (this.dialogMode === 'create') {
|
||||
if (this.dialogMode === EditDialogMode.CREATE) {
|
||||
this.objectForm.get('permissions_form')?.setValue({
|
||||
owner: this.settingsService.currentUser.id,
|
||||
})
|
||||
@@ -87,15 +92,11 @@ export abstract class EditDialogComponent<
|
||||
return $localize`Edit item`
|
||||
}
|
||||
|
||||
getSaveErrorMessage(error: string) {
|
||||
return $localize`Could not save element: ${error}`
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
switch (this.dialogMode) {
|
||||
case 'create':
|
||||
case EditDialogMode.CREATE:
|
||||
return this.getCreateTitle()
|
||||
case 'edit':
|
||||
case EditDialogMode.EDIT:
|
||||
return this.getEditTitle()
|
||||
default:
|
||||
break
|
||||
@@ -127,10 +128,10 @@ export abstract class EditDialogComponent<
|
||||
var newObject = Object.assign(Object.assign({}, this.object), formValues)
|
||||
var serverResponse: Observable<T>
|
||||
switch (this.dialogMode) {
|
||||
case 'create':
|
||||
case EditDialogMode.CREATE:
|
||||
serverResponse = this.service.create(newObject)
|
||||
break
|
||||
case 'edit':
|
||||
case EditDialogMode.EDIT:
|
||||
serverResponse = this.service.update(newObject)
|
||||
default:
|
||||
break
|
||||
|
@@ -0,0 +1,57 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { GroupEditDialogComponent } from './group-edit-dialog.component'
|
||||
import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
|
||||
|
||||
describe('GroupEditDialogComponent', () => {
|
||||
let component: GroupEditDialogComponent
|
||||
let fixture: ComponentFixture<GroupEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
GroupEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
PermissionsSelectComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(GroupEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,117 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
discardPeriodicTasks,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { MailAccountEditDialogComponent } from './mail-account-edit-dialog.component'
|
||||
import { PasswordComponent } from '../../input/password/password.component'
|
||||
import { CheckComponent } from '../../input/check/check.component'
|
||||
import { IMAPSecurity } from 'src/app/data/paperless-mail-account'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
describe('MailAccountEditDialogComponent', () => {
|
||||
let component: MailAccountEditDialogComponent
|
||||
let fixture: ComponentFixture<MailAccountEditDialogComponent>
|
||||
let httpController: HttpTestingController
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
MailAccountEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
CheckComponent,
|
||||
PermissionsFormComponent,
|
||||
PasswordComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
httpController = TestBed.inject(HttpTestingController)
|
||||
|
||||
fixture = TestBed.createComponent(MailAccountEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support test mail account and show appropriate expiring alert', fakeAsync(() => {
|
||||
component.object = {
|
||||
name: 'example',
|
||||
imap_server: 'imap.example.com',
|
||||
username: 'user',
|
||||
password: 'pass',
|
||||
imap_port: 443,
|
||||
imap_security: IMAPSecurity.SSL,
|
||||
is_token: false,
|
||||
}
|
||||
|
||||
// success
|
||||
component.test()
|
||||
httpController
|
||||
.expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
|
||||
.flush({ success: true })
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).toContain(
|
||||
'Successfully connected'
|
||||
)
|
||||
tick(6000)
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).not.toContain(
|
||||
'Successfully connected'
|
||||
)
|
||||
|
||||
// not success
|
||||
component.test()
|
||||
httpController
|
||||
.expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
|
||||
.flush({ success: false })
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).toContain('Unable to connect')
|
||||
|
||||
// error
|
||||
component.test()
|
||||
httpController
|
||||
.expectOne(`${environment.apiBaseUrl}mail_accounts/test/`)
|
||||
.flush({}, { status: 500, statusText: 'error' })
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).toContain('Unable to connect')
|
||||
tick(6000)
|
||||
}))
|
||||
})
|
@@ -0,0 +1,113 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { MailRuleEditDialogComponent } from './mail-rule-edit-dialog.component'
|
||||
import { NumberComponent } from '../../input/number/number.component'
|
||||
import { TagsComponent } from '../../input/tags/tags.component'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { MailAccountService } from 'src/app/services/rest/mail-account.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { of } from 'rxjs'
|
||||
import {
|
||||
MailAction,
|
||||
MailMetadataCorrespondentOption,
|
||||
} from 'src/app/data/paperless-mail-rule'
|
||||
|
||||
describe('MailRuleEditDialogComponent', () => {
|
||||
let component: MailRuleEditDialogComponent
|
||||
let fixture: ComponentFixture<MailRuleEditDialogComponent>
|
||||
let accountService: MailAccountService
|
||||
let correspondentService: CorrespondentService
|
||||
let documentTypeService: DocumentTypeService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
MailRuleEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
NumberComponent,
|
||||
TagsComponent,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
{
|
||||
provide: MailAccountService,
|
||||
useValue: {
|
||||
listAll: () => of([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CorrespondentService,
|
||||
useValue: {
|
||||
listAll: () => of([]),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DocumentTypeService,
|
||||
useValue: {
|
||||
listAll: () => of([]),
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(MailRuleEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support optional fields', () => {
|
||||
expect(component.showCorrespondentField).toBeFalsy()
|
||||
component.objectForm
|
||||
.get('assign_correspondent_from')
|
||||
.setValue(MailMetadataCorrespondentOption.FromCustom)
|
||||
expect(component.showCorrespondentField).toBeTruthy()
|
||||
|
||||
expect(component.showActionParamField).toBeFalsy()
|
||||
component.objectForm.get('action').setValue(MailAction.Move)
|
||||
expect(component.showActionParamField).toBeTruthy()
|
||||
component.objectForm.get('action').setValue('')
|
||||
expect(component.showActionParamField).toBeFalsy()
|
||||
component.objectForm.get('action').setValue(MailAction.Tag)
|
||||
expect(component.showActionParamField).toBeTruthy()
|
||||
|
||||
// coverage of optional chaining
|
||||
component.objectForm = null
|
||||
expect(component.showCorrespondentField).toBeFalsy()
|
||||
expect(component.showActionParamField).toBeFalsy()
|
||||
})
|
||||
})
|
@@ -0,0 +1,57 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { StoragePathEditDialogComponent } from './storage-path-edit-dialog.component'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
|
||||
describe('StoragePathEditDialogComponent', () => {
|
||||
let component: StoragePathEditDialogComponent
|
||||
let fixture: ComponentFixture<StoragePathEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
StoragePathEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
SafeHtmlPipe,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(StoragePathEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,59 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { TagEditDialogComponent } from './tag-edit-dialog.component'
|
||||
import { ColorComponent } from '../../input/color/color.component'
|
||||
import { CheckComponent } from '../../input/check/check.component'
|
||||
|
||||
describe('TagEditDialogComponent', () => {
|
||||
let component: TagEditDialogComponent
|
||||
let fixture: ComponentFixture<TagEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
TagEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PermissionsFormComponent,
|
||||
ColorComponent,
|
||||
CheckComponent,
|
||||
],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(TagEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,115 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { EditDialogMode } from '../edit-dialog.component'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { SelectComponent } from '../../input/select/select.component'
|
||||
import {
|
||||
AbstractControl,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { TextComponent } from '../../input/text/text.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component'
|
||||
import { UserEditDialogComponent } from './user-edit-dialog.component'
|
||||
import { PasswordComponent } from '../../input/password/password.component'
|
||||
import { PermissionsSelectComponent } from '../../permissions-select/permissions-select.component'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { of } from 'rxjs'
|
||||
|
||||
describe('UserEditDialogComponent', () => {
|
||||
let component: UserEditDialogComponent
|
||||
let fixture: ComponentFixture<UserEditDialogComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
UserEditDialogComponent,
|
||||
IfPermissionsDirective,
|
||||
IfOwnerDirective,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
PasswordComponent,
|
||||
PermissionsFormComponent,
|
||||
PermissionsSelectComponent,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
{
|
||||
provide: GroupService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
permissions: ['dummy_perms'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(UserEditDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support create and edit modes', () => {
|
||||
component.dialogMode = EditDialogMode.CREATE
|
||||
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
|
||||
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
|
||||
fixture.detectChanges()
|
||||
expect(createTitleSpy).toHaveBeenCalled()
|
||||
expect(editTitleSpy).not.toHaveBeenCalled()
|
||||
component.dialogMode = EditDialogMode.EDIT
|
||||
fixture.detectChanges()
|
||||
expect(editTitleSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should disable user permissions select on toggle superuser', () => {
|
||||
const control: AbstractControl =
|
||||
component.objectForm.get('user_permissions')
|
||||
expect(control.disabled).toBeFalsy()
|
||||
component.objectForm.get('is_superuser').setValue(true)
|
||||
component.onToggleSuperUser()
|
||||
expect(control.disabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should update inherited permissions', () => {
|
||||
component.objectForm.get('groups').setValue(null)
|
||||
expect(component.inheritedPermissions).toEqual([])
|
||||
component.objectForm.get('groups').setValue([1])
|
||||
expect(component.inheritedPermissions).toEqual(['dummy_perms'])
|
||||
component.objectForm.get('groups').setValue([2])
|
||||
expect(component.inheritedPermissions).toEqual([])
|
||||
})
|
||||
|
||||
it('should detect whether password was changed in form on save', () => {
|
||||
component.objectForm.get('password').setValue(null)
|
||||
component.save()
|
||||
expect(component.passwordIsSet).toBeFalsy()
|
||||
|
||||
// unchanged pw
|
||||
component.objectForm.get('password').setValue('*******')
|
||||
component.save()
|
||||
expect(component.passwordIsSet).toBeFalsy()
|
||||
|
||||
// unchanged pw
|
||||
component.objectForm.get('password').setValue('helloworld')
|
||||
component.save()
|
||||
expect(component.passwordIsSet).toBeTruthy()
|
||||
})
|
||||
})
|
@@ -34,7 +34,7 @@
|
||||
<div *ngIf="selectionModel.items" class="items" #buttonItems>
|
||||
<ng-container *ngFor="let item of selectionModel.itemsSorted | filter: filterText; let i = index">
|
||||
<app-toggleable-dropdown-button
|
||||
*ngIf="allowSelectNone || item.id" [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i)" [disabled]="disabled">
|
||||
*ngIf="allowSelectNone || item.id" [item]="item" [hideCount]="hideCount(item)" [state]="selectionModel.get(item.id)" [count]="getUpdatedDocumentCount(item.id)" (toggle)="selectionModel.toggle(item.id)" (exclude)="excludeClicked(item.id)" (click)="setButtonItemIndex(i - 1)" [disabled]="disabled">
|
||||
</app-toggleable-dropdown-button>
|
||||
</ng-container>
|
||||
</div>
|
||||
|
@@ -0,0 +1,487 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import {
|
||||
ChangedItems,
|
||||
FilterableDropdownComponent,
|
||||
FilterableDropdownSelectionModel,
|
||||
Intersection,
|
||||
LogicalOperator,
|
||||
} from './filterable-dropdown.component'
|
||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
} from 'src/app/data/matching-model'
|
||||
import {
|
||||
ToggleableDropdownButtonComponent,
|
||||
ToggleableItemState,
|
||||
} from './toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||
import { TagComponent } from '../tag/tag.component'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
|
||||
const items: PaperlessTag[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Tag1',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Tag2',
|
||||
is_inbox_tag: true,
|
||||
matching_algorithm: MATCH_ALL,
|
||||
match: 'str',
|
||||
},
|
||||
]
|
||||
|
||||
const nullItem = {
|
||||
id: null,
|
||||
name: 'Not assigned',
|
||||
}
|
||||
|
||||
let selectionModel: FilterableDropdownSelectionModel
|
||||
|
||||
describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => {
|
||||
let component: FilterableDropdownComponent
|
||||
let fixture: ComponentFixture<FilterableDropdownComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
FilterableDropdownComponent,
|
||||
FilterPipe,
|
||||
ToggleableDropdownButtonComponent,
|
||||
TagComponent,
|
||||
ClearableBadgeComponent,
|
||||
],
|
||||
providers: [FilterPipe],
|
||||
imports: [NgbModule, FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(FilterableDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
selectionModel = new FilterableDropdownSelectionModel()
|
||||
})
|
||||
|
||||
it('should sanitize title', () => {
|
||||
expect(component.name).toBeNull()
|
||||
component.title = 'Foo Bar'
|
||||
expect(component.name).toEqual('foo_bar')
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
expect(selectionModel.getSelectedItems()).toHaveLength(1)
|
||||
expect(selectionModel.isDirty()).toBeTruthy()
|
||||
component.reset()
|
||||
expect(selectionModel.getSelectedItems()).toHaveLength(0)
|
||||
expect(selectionModel.isDirty()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should report document counts', () => {
|
||||
component.documentCounts = [
|
||||
{
|
||||
id: items[0].id,
|
||||
document_count: 12,
|
||||
},
|
||||
]
|
||||
expect(component.getUpdatedDocumentCount(items[0].id)).toEqual(12)
|
||||
expect(component.getUpdatedDocumentCount(items[1].id)).toBeUndefined() // coverate of optional chaining
|
||||
})
|
||||
|
||||
it('should emit change when items selected', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
expect(newModel).toBeUndefined()
|
||||
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
expect(selectionModel.isDirty()).toBeTruthy()
|
||||
expect(newModel.getSelectedItems()).toEqual([items[0]])
|
||||
expect(newModel.getExcludedItems()).toEqual([])
|
||||
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
expect(newModel.getSelectedItems()).toEqual([])
|
||||
|
||||
expect(component.items).toEqual([nullItem, ...items])
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
expect(newModel).toBeUndefined()
|
||||
selectionModel.toggle(items[0].id)
|
||||
expect(newModel.getSelectedItems()).toEqual([items[0]])
|
||||
})
|
||||
|
||||
it('should emit change when items excluded', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Excluded)
|
||||
expect(newModel.getSelectedItems()).toEqual([])
|
||||
expect(newModel.getExcludedItems()).toEqual([items[0]])
|
||||
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
expect(newModel.getSelectedItems()).toEqual([])
|
||||
expect(newModel.getExcludedItems()).toEqual([])
|
||||
})
|
||||
|
||||
it('should exclude items when excluded and not editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
component.excludeClicked(items[0].id)
|
||||
expect(selectionModel.getSelectedItems()).toEqual([])
|
||||
expect(selectionModel.getExcludedItems()).toEqual([items[0]])
|
||||
})
|
||||
|
||||
it('should toggle when items excluded and editing', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.set(items[0].id, ToggleableItemState.NotSelected)
|
||||
component.excludeClicked(items[0].id)
|
||||
expect(selectionModel.getSelectedItems()).toEqual([items[0]])
|
||||
expect(selectionModel.getExcludedItems()).toEqual([])
|
||||
})
|
||||
|
||||
it('should hide count for item if adding will increase size of set', () => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.hideCount(items[0])).toBeFalsy()
|
||||
selectionModel.logicalOperator = LogicalOperator.Or
|
||||
expect(component.hideCount(items[0])).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should enforce single select when editing', () => {
|
||||
component.editing = true
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
|
||||
expect(selectionModel.singleSelect).toEqual(true)
|
||||
selectionModel.toggle(items[0].id)
|
||||
selectionModel.toggle(items[1].id)
|
||||
expect(newModel.getSelectedItems()).toEqual([items[1]])
|
||||
})
|
||||
|
||||
it('should support manyToOne selecting', () => {
|
||||
component.items = items
|
||||
selectionModel.manyToOne = false
|
||||
component.selectionModel = selectionModel
|
||||
component.manyToOne = true
|
||||
expect(component.manyToOne).toBeTruthy()
|
||||
let newModel: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe((model) => (newModel = model))
|
||||
|
||||
expect(selectionModel.singleSelect).toEqual(false)
|
||||
selectionModel.toggle(items[0].id)
|
||||
selectionModel.toggle(items[1].id)
|
||||
expect(newModel.getSelectedItems()).toEqual([items[0], items[1]])
|
||||
})
|
||||
|
||||
it('should dynamically enable / disable modifier toggle', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
selectionModel.toggle(null)
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
component.manyToOne = true
|
||||
expect(component.modifierToggleEnabled).toBeFalsy()
|
||||
selectionModel.toggle(items[0].id)
|
||||
selectionModel.toggle(items[1].id)
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should apply changes and close when apply button clicked', () => {
|
||||
component.items = items
|
||||
component.editing = true
|
||||
component.selectionModel = selectionModel
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
selectionModel.toggle(items[0].id)
|
||||
fixture.detectChanges()
|
||||
expect(component.modelIsDirty).toBeTruthy()
|
||||
let applyResult: ChangedItems
|
||||
const closeSpy = jest.spyOn(component.dropdown, 'close')
|
||||
component.apply.subscribe((result) => (applyResult = result))
|
||||
const applyButton = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).find((b) => b.textContent.includes('Apply'))
|
||||
applyButton.dispatchEvent(new MouseEvent('click'))
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
expect(applyResult).toEqual({ itemsToAdd: [items[0]], itemsToRemove: [] })
|
||||
})
|
||||
|
||||
it('should apply on close if enabled', () => {
|
||||
component.items = items
|
||||
component.editing = true
|
||||
component.applyOnClose = true
|
||||
component.selectionModel = selectionModel
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
selectionModel.toggle(items[0].id)
|
||||
fixture.detectChanges()
|
||||
expect(component.modelIsDirty).toBeTruthy()
|
||||
let applyResult: ChangedItems
|
||||
component.apply.subscribe((result) => (applyResult = result))
|
||||
component.dropdown.close()
|
||||
expect(applyResult).toEqual({ itemsToAdd: [items[0]], itemsToRemove: [] })
|
||||
})
|
||||
|
||||
it('should focus text filter on open, support filtering, clear on close', fakeAsync(() => {
|
||||
component.items = items
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
expect(document.activeElement).toEqual(
|
||||
component.listFilterTextInput.nativeElement
|
||||
)
|
||||
expect(
|
||||
Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).filter((b) => b.textContent.includes('Tag'))
|
||||
).toHaveLength(2)
|
||||
component.filterText = 'Tag2'
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).filter((b) => b.textContent.includes('Tag'))
|
||||
).toHaveLength(1)
|
||||
component.dropdown.close()
|
||||
expect(component.filterText).toHaveLength(0)
|
||||
}))
|
||||
|
||||
it('should toggle & close on enter inside filter field if 1 item remains', fakeAsync(() => {
|
||||
component.items = items
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
component.filterText = 'Tag2'
|
||||
fixture.detectChanges()
|
||||
const closeSpy = jest.spyOn(component.dropdown, 'close')
|
||||
component.listFilterTextInput.nativeElement.dispatchEvent(
|
||||
new KeyboardEvent('keyup', { key: 'Enter' })
|
||||
)
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([items[1]])
|
||||
tick(300)
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
}))
|
||||
|
||||
it('should apply & close on enter inside filter field if 1 item remains if editing', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.editing = true
|
||||
let applyResult: ChangedItems
|
||||
component.apply.subscribe((result) => (applyResult = result))
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([])
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
component.filterText = 'Tag2'
|
||||
fixture.detectChanges()
|
||||
component.listFilterTextInput.nativeElement.dispatchEvent(
|
||||
new KeyboardEvent('keyup', { key: 'Enter' })
|
||||
)
|
||||
expect(component.selectionModel.getSelectedItems()).toEqual([items[1]])
|
||||
tick(300)
|
||||
expect(applyResult).toEqual({ itemsToAdd: [items[1]], itemsToRemove: [] })
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
const filterInputEl: HTMLInputElement =
|
||||
component.listFilterTextInput.nativeElement
|
||||
expect(document.activeElement).toEqual(filterInputEl)
|
||||
const itemButtons = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).filter((b) => b.textContent.includes('Tag'))
|
||||
filterInputEl.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[0])
|
||||
itemButtons[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[1])
|
||||
itemButtons[1].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[0])
|
||||
itemButtons[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(filterInputEl)
|
||||
filterInputEl.value = 'foo'
|
||||
component.filterText = 'foo'
|
||||
|
||||
// dont move focus if we're traversing the field
|
||||
filterInputEl.selectionStart = 1
|
||||
expect(document.activeElement).toEqual(filterInputEl)
|
||||
|
||||
// now we're at end, so move focus
|
||||
filterInputEl.selectionStart = 3
|
||||
filterInputEl.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[0])
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after tab keyboard navigation', fakeAsync(() => {
|
||||
component.items = items
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
const filterInputEl: HTMLInputElement =
|
||||
component.listFilterTextInput.nativeElement
|
||||
expect(document.activeElement).toEqual(filterInputEl)
|
||||
const itemButtons = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).filter((b) => b.textContent.includes('Tag'))
|
||||
filterInputEl.dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
|
||||
)
|
||||
itemButtons[0].focus() // normally handled by browser
|
||||
itemButtons[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'Tab', bubbles: true })
|
||||
)
|
||||
itemButtons[1].focus() // normally handled by browser
|
||||
itemButtons[1].dispatchEvent(
|
||||
new KeyboardEvent('keydown', {
|
||||
key: 'Tab',
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
})
|
||||
)
|
||||
itemButtons[0].focus() // normally handled by browser
|
||||
itemButtons[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[1])
|
||||
}))
|
||||
|
||||
it('should support arrow keyboard navigation after click', fakeAsync(() => {
|
||||
component.items = items
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
const filterInputEl: HTMLInputElement =
|
||||
component.listFilterTextInput.nativeElement
|
||||
expect(document.activeElement).toEqual(filterInputEl)
|
||||
const itemButtons = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('button')
|
||||
).filter((b) => b.textContent.includes('Tag'))
|
||||
fixture.nativeElement
|
||||
.querySelector('app-toggleable-dropdown-button')
|
||||
.dispatchEvent(new MouseEvent('click'))
|
||||
itemButtons[0].focus() // normally handled by browser
|
||||
expect(document.activeElement).toEqual(itemButtons[0])
|
||||
itemButtons[0].dispatchEvent(
|
||||
new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })
|
||||
)
|
||||
expect(document.activeElement).toEqual(itemButtons[1])
|
||||
}))
|
||||
|
||||
it('should toggle logical operator', fakeAsync(() => {
|
||||
component.items = items
|
||||
component.manyToOne = true
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
component.selectionModel = selectionModel
|
||||
let changedResult: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe(
|
||||
(result) => (changedResult = result)
|
||||
)
|
||||
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
const operatorButtons: HTMLInputElement[] = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('input')
|
||||
).filter((b) => ['and', 'or'].includes(b.value))
|
||||
expect(operatorButtons[0].checked).toBeTruthy()
|
||||
operatorButtons[1].dispatchEvent(new MouseEvent('click'))
|
||||
fixture.detectChanges()
|
||||
expect(selectionModel.logicalOperator).toEqual(LogicalOperator.Or)
|
||||
expect(changedResult.logicalOperator).toEqual(LogicalOperator.Or)
|
||||
}))
|
||||
|
||||
it('should toggle intersection include / exclude', fakeAsync(() => {
|
||||
component.items = items
|
||||
selectionModel.set(items[0].id, ToggleableItemState.Selected)
|
||||
selectionModel.set(items[1].id, ToggleableItemState.Selected)
|
||||
component.selectionModel = selectionModel
|
||||
let changedResult: FilterableDropdownSelectionModel
|
||||
component.selectionModelChange.subscribe(
|
||||
(result) => (changedResult = result)
|
||||
)
|
||||
|
||||
fixture.nativeElement
|
||||
.querySelector('button')
|
||||
.dispatchEvent(new MouseEvent('click')) // open
|
||||
fixture.detectChanges()
|
||||
tick(100)
|
||||
|
||||
expect(component.modifierToggleEnabled).toBeTruthy()
|
||||
const intersectionButtons: HTMLInputElement[] = Array.from(
|
||||
(fixture.nativeElement as HTMLDivElement).querySelectorAll('input')
|
||||
).filter((b) => ['include', 'exclude'].includes(b.value))
|
||||
expect(intersectionButtons[0].checked).toBeTruthy()
|
||||
intersectionButtons[1].dispatchEvent(new MouseEvent('click'))
|
||||
fixture.detectChanges()
|
||||
expect(selectionModel.intersection).toEqual(Intersection.Exclude)
|
||||
expect(changedResult.intersection).toEqual(Intersection.Exclude)
|
||||
expect(changedResult.getSelectedItems()).toEqual([])
|
||||
expect(changedResult.getExcludedItems()).toEqual(items)
|
||||
}))
|
||||
|
||||
it('FilterableDropdownSelectionModel should sort items by state', () => {
|
||||
component.items = items
|
||||
component.selectionModel = selectionModel
|
||||
selectionModel.toggle(items[1].id)
|
||||
selectionModel.apply()
|
||||
expect(selectionModel.itemsSorted).toEqual([nullItem, items[1], items[0]])
|
||||
})
|
||||
})
|
@@ -96,7 +96,7 @@ export class FilterableDropdownSelectionModel {
|
||||
toggle(id: number, fireEvent = true) {
|
||||
let state = this.temporarySelectionStates.get(id)
|
||||
if (
|
||||
state == null ||
|
||||
state == undefined ||
|
||||
(state != ToggleableItemState.Selected &&
|
||||
state != ToggleableItemState.Excluded)
|
||||
) {
|
||||
|
@@ -0,0 +1,79 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
ToggleableDropdownButtonComponent,
|
||||
ToggleableItemState,
|
||||
} from './toggleable-dropdown-button.component'
|
||||
import { TagComponent } from '../../tag/tag.component'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
|
||||
describe('ToggleableDropdownButtonComponent', () => {
|
||||
let component: ToggleableDropdownButtonComponent
|
||||
let fixture: ComponentFixture<ToggleableDropdownButtonComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ToggleableDropdownButtonComponent, TagComponent],
|
||||
providers: [],
|
||||
imports: [],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ToggleableDropdownButtonComponent)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
it('should recognize a tag', () => {
|
||||
component.item = {
|
||||
id: 1,
|
||||
name: 'Test Tag',
|
||||
is_inbox_tag: false,
|
||||
} as PaperlessTag
|
||||
|
||||
fixture.detectChanges()
|
||||
expect(component.isTag).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should report toggled state', () => {
|
||||
expect(component.isChecked()).toBeFalsy()
|
||||
expect(component.isPartiallyChecked()).toBeFalsy()
|
||||
expect(component.isExcluded()).toBeFalsy()
|
||||
|
||||
component.state = ToggleableItemState.Selected
|
||||
expect(component.isChecked()).toBeTruthy()
|
||||
expect(component.isPartiallyChecked()).toBeFalsy()
|
||||
expect(component.isExcluded()).toBeFalsy()
|
||||
|
||||
component.state = ToggleableItemState.PartiallySelected
|
||||
expect(component.isPartiallyChecked()).toBeTruthy()
|
||||
expect(component.isChecked()).toBeFalsy()
|
||||
expect(component.isExcluded()).toBeFalsy()
|
||||
|
||||
component.state = ToggleableItemState.Excluded
|
||||
expect(component.isExcluded()).toBeTruthy()
|
||||
expect(component.isChecked()).toBeFalsy()
|
||||
expect(component.isPartiallyChecked()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should emit exclude event when selected and then toggled', () => {
|
||||
let excludeResult
|
||||
let toggleResult
|
||||
component.state = ToggleableItemState.Selected
|
||||
component.exclude.subscribe(() => (excludeResult = true))
|
||||
component.toggle.subscribe(() => (toggleResult = true))
|
||||
const button = fixture.nativeElement.querySelector('button')
|
||||
button.dispatchEvent(new MouseEvent('click'))
|
||||
expect(excludeResult).toBeTruthy()
|
||||
expect(toggleResult).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should emit toggle event when not selected and then toggled', () => {
|
||||
let excludeResult
|
||||
let toggleResult
|
||||
component.state = ToggleableItemState.Excluded
|
||||
component.exclude.subscribe(() => (excludeResult = true))
|
||||
component.toggle.subscribe(() => (toggleResult = true))
|
||||
const button = fixture.nativeElement.querySelector('button')
|
||||
button.dispatchEvent(new MouseEvent('click'))
|
||||
expect(excludeResult).toBeFalsy()
|
||||
expect(toggleResult).toBeTruthy()
|
||||
})
|
||||
})
|
@@ -0,0 +1,55 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { AbstractInputComponent } from './abstract-input'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div>
|
||||
<input
|
||||
#inputField
|
||||
type="text"
|
||||
class="form-control"
|
||||
[class.is-invalid]="error"
|
||||
[id]="inputId"
|
||||
[(ngModel)]="value"
|
||||
(change)="onChange(value)"
|
||||
[disabled]="disabled"
|
||||
/>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class TestComponent extends AbstractInputComponent<string> {
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
}
|
||||
|
||||
describe(`AbstractInputComponent`, () => {
|
||||
let component: TestComponent
|
||||
let fixture: ComponentFixture<TestComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TestComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should assign uuid', () => {
|
||||
component.ngOnInit()
|
||||
expect(component.inputId).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should support focus', () => {
|
||||
const focusSpy = jest.spyOn(component.inputField.nativeElement, 'focus')
|
||||
component.focus()
|
||||
expect(focusSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -1,5 +1,5 @@
|
||||
<div class="mb-3 form-check">
|
||||
<input type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
||||
<input #inputField type="checkbox" class="form-check-input" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" (blur)="onTouched()" [disabled]="disabled">
|
||||
<label class="form-check-label" [for]="inputId">{{title}}</label>
|
||||
<div *ngIf="hint" class="form-text text-muted">{{hint}}</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,39 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { CheckComponent } from './check.component'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
|
||||
describe('CheckComponent', () => {
|
||||
let component: CheckComponent
|
||||
let fixture: ComponentFixture<CheckComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [CheckComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(CheckComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of checkbox', () => {
|
||||
input.checked = true
|
||||
input.dispatchEvent(new Event('change'))
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toBeTruthy()
|
||||
|
||||
input.checked = false
|
||||
input.dispatchEvent(new Event('change'))
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toBeFalsy()
|
||||
})
|
||||
})
|
@@ -1,6 +1,5 @@
|
||||
import { Component, forwardRef, Input, OnInit } from '@angular/core'
|
||||
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { Component, forwardRef } from '@angular/core'
|
||||
import { NG_VALUE_ACCESSOR } from '@angular/forms'
|
||||
import { AbstractInputComponent } from '../abstract-input'
|
||||
|
||||
@Component({
|
||||
|
@@ -11,7 +11,7 @@
|
||||
|
||||
</ng-template>
|
||||
|
||||
<input class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
|
||||
<input #inputField class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [autoClose]="'outside'" [ngbPopover]="popContent" placement="bottom" popoverClass="shadow">
|
||||
|
||||
<button class="btn btn-outline-secondary" type="button" (click)="randomize()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-dice-5" viewBox="0 0 16 16">
|
||||
|
@@ -0,0 +1,72 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { ColorComponent } from './color.component'
|
||||
import { NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { ColorSliderModule } from 'ngx-color/slider'
|
||||
|
||||
describe('ColorComponent', () => {
|
||||
let component: ColorComponent
|
||||
let fixture: ComponentFixture<ColorComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ColorComponent],
|
||||
providers: [],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbPopoverModule,
|
||||
ColorSliderModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ColorComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of input', () => {
|
||||
input.value = '#ff0000'
|
||||
component.colorChanged(input.value)
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toEqual('#ff0000')
|
||||
})
|
||||
|
||||
it('should set swatch color', () => {
|
||||
const swatch: HTMLSpanElement = fixture.nativeElement.querySelector(
|
||||
'span.input-group-text'
|
||||
)
|
||||
expect(swatch.style.backgroundColor).toEqual('')
|
||||
component.value = '#ff0000'
|
||||
fixture.detectChanges()
|
||||
expect(swatch.style.backgroundColor).toEqual('rgb(255, 0, 0)')
|
||||
})
|
||||
|
||||
it('should show color slider popover', () => {
|
||||
component.value = '#ff0000'
|
||||
input.dispatchEvent(new MouseEvent('click'))
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('ngb-popover-window')
|
||||
).not.toBeUndefined()
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('color-slider')
|
||||
).not.toBeUndefined()
|
||||
fixture.nativeElement
|
||||
.querySelector('color-slider')
|
||||
.dispatchEvent(new Event('change'))
|
||||
})
|
||||
|
||||
it('should allow randomize color and update value', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
component.randomize()
|
||||
expect(component.value).not.toBeUndefined()
|
||||
})
|
||||
})
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
|
||||
<input #inputField class="form-control" [class.is-invalid]="error" [placeholder]="placeholder" [id]="inputId" maxlength="10"
|
||||
(dateSelect)="onChange(value)" (change)="onChange(value)" (keypress)="onKeyPress($event)" (paste)="onPaste($event)"
|
||||
name="dp" [(ngModel)]="value" ngbDatepicker #datePicker="ngbDatepicker" #datePickerContent="ngModel" [disabled]="disabled">
|
||||
<button class="btn btn-outline-secondary calendar" (click)="datePicker.toggle()" type="button" [disabled]="disabled">
|
||||
@@ -9,7 +9,7 @@
|
||||
<path d="M3.5 0a.5.5 0 0 1 .5.5V1h8V.5a.5.5 0 0 1 1 0V1h1a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V3a2 2 0 0 1 2-2h1V.5a.5.5 0 0 1 .5-.5zM1 4v10a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V4H1z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" i18n-title title="Filter documents with this {{title}}">
|
||||
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="this.value === null" title="{{ fitlerButtonTitle }}">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#filter" />
|
||||
</svg>
|
||||
|
@@ -0,0 +1,103 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { DateComponent } from './date.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import {
|
||||
NgbDateParserFormatter,
|
||||
NgbDatepickerModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { LocalizedDateParserFormatter } from 'src/app/utils/ngb-date-parser-formatter'
|
||||
|
||||
describe('DateComponent', () => {
|
||||
let component: DateComponent
|
||||
let fixture: ComponentFixture<DateComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DateComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: NgbDateParserFormatter,
|
||||
useClass: LocalizedDateParserFormatter,
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientTestingModule,
|
||||
NgbDatepickerModule,
|
||||
RouterTestingModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(DateComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of input field', () => {
|
||||
input.value = '5/14/20'
|
||||
input.dispatchEvent(new Event('change'))
|
||||
fixture.detectChanges()
|
||||
expect(component.value).toEqual({ day: 14, month: 5, year: 2020 })
|
||||
})
|
||||
|
||||
it('should use localzed placeholder from settings', () => {
|
||||
component.ngOnInit()
|
||||
expect(component.placeholder).toEqual('mm/dd/yyyy')
|
||||
})
|
||||
|
||||
it('should support suggestions', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
component.suggestions = ['2023-05-31', '2014-05-14']
|
||||
fixture.detectChanges()
|
||||
const suggestionAnchor: HTMLAnchorElement =
|
||||
fixture.nativeElement.querySelector('a')
|
||||
suggestionAnchor.click()
|
||||
expect(component.value).toEqual({ day: 31, month: 5, year: 2023 })
|
||||
})
|
||||
|
||||
it('should limit keyboard events', () => {
|
||||
let event: KeyboardEvent = new KeyboardEvent('keypress', {
|
||||
key: '9',
|
||||
})
|
||||
let eventSpy = jest.spyOn(event, 'preventDefault')
|
||||
input.dispatchEvent(event)
|
||||
expect(eventSpy).not.toHaveBeenCalled()
|
||||
|
||||
event = new KeyboardEvent('keypress', {
|
||||
key: '{',
|
||||
})
|
||||
eventSpy = jest.spyOn(event, 'preventDefault')
|
||||
input.dispatchEvent(event)
|
||||
expect(eventSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support paste', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
const date = '5/4/20'
|
||||
const clipboardData = {
|
||||
dropEffect: null,
|
||||
effectAllowed: null,
|
||||
files: null,
|
||||
items: null,
|
||||
types: null,
|
||||
clearData: null,
|
||||
getData: () => date,
|
||||
setData: null,
|
||||
setDragImage: null,
|
||||
}
|
||||
const event = new Event('paste')
|
||||
event['clipboardData'] = clipboardData
|
||||
input.dispatchEvent(event)
|
||||
expect(component.value).toEqual({ day: 4, month: 5, year: 2020 })
|
||||
})
|
||||
})
|
@@ -98,4 +98,8 @@ export class DateComponent
|
||||
onFilterDocuments() {
|
||||
this.filterDocuments.emit([this.ngbDateParserFormatter.parse(this.value)])
|
||||
}
|
||||
|
||||
get filterButtonTitle() {
|
||||
return $localize`Filter documents with this ${this.title}`
|
||||
}
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<div class="mb-3">
|
||||
<label class="form-label" [for]="inputId">{{title}}</label>
|
||||
<div class="input-group" [class.is-invalid]="error">
|
||||
<input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
|
||||
<input #inputField type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error" [disabled]="disabled">
|
||||
<button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="disabled">+1</button>
|
||||
</div>
|
||||
<div class="invalid-feedback">
|
||||
|
@@ -0,0 +1,79 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NumberComponent } from './number.component'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { of } from 'rxjs'
|
||||
|
||||
describe('NumberComponent', () => {
|
||||
let component: NumberComponent
|
||||
let fixture: ComponentFixture<NumberComponent>
|
||||
let input: HTMLInputElement
|
||||
let documentService: DocumentService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [NumberComponent],
|
||||
providers: [DocumentService],
|
||||
imports: [FormsModule, ReactiveFormsModule, HttpClientTestingModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(NumberComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
// TODO: why doesnt this work?
|
||||
// it('should support use of input field', () => {
|
||||
// expect(component.value).toBeUndefined()
|
||||
// input.stepUp()
|
||||
// console.log(input.value);
|
||||
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
// expect(component.value).toEqual('3')
|
||||
// })
|
||||
|
||||
it('should support +1 ASN', () => {
|
||||
const listAllSpy = jest.spyOn(documentService, 'listFiltered')
|
||||
listAllSpy
|
||||
.mockReturnValueOnce(
|
||||
of({
|
||||
count: 1,
|
||||
all: [1],
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
archive_serial_number: 1000,
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.mockReturnValueOnce(
|
||||
of({
|
||||
count: 0,
|
||||
all: [],
|
||||
results: [],
|
||||
})
|
||||
)
|
||||
expect(component.value).toBeUndefined()
|
||||
component.nextAsn()
|
||||
expect(component.value).toEqual(1001)
|
||||
|
||||
// this time results are empty
|
||||
component.value = undefined
|
||||
component.nextAsn()
|
||||
expect(component.value).toEqual(1)
|
||||
|
||||
component.value = 1002
|
||||
component.nextAsn()
|
||||
expect(component.value).toEqual(1002)
|
||||
})
|
||||
})
|
@@ -0,0 +1,36 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { PasswordComponent } from './password.component'
|
||||
|
||||
describe('PasswordComponent', () => {
|
||||
let component: PasswordComponent
|
||||
let fixture: ComponentFixture<PasswordComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PasswordComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(PasswordComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
// TODO: why doesnt this work?
|
||||
// input.value = 'foo'
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
// expect(component.value).toEqual('foo')
|
||||
})
|
||||
})
|
@@ -0,0 +1,66 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { PermissionsFormComponent } from './permissions-form.component'
|
||||
import { SelectComponent } from '../../select/select.component'
|
||||
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PermissionsGroupComponent } from '../permissions-group/permissions-group.component'
|
||||
import { PermissionsUserComponent } from '../permissions-user/permissions-user.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
|
||||
describe('PermissionsFormComponent', () => {
|
||||
let component: PermissionsFormComponent
|
||||
let fixture: ComponentFixture<PermissionsFormComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
PermissionsFormComponent,
|
||||
SelectComponent,
|
||||
PermissionsGroupComponent,
|
||||
PermissionsUserComponent,
|
||||
],
|
||||
providers: [],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbAccordionModule,
|
||||
HttpClientTestingModule,
|
||||
NgSelectModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(PermissionsFormComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support use of select for owner', () => {
|
||||
const changeSpy = jest.spyOn(component, 'onChange')
|
||||
component.ngOnInit()
|
||||
component.users = [
|
||||
{
|
||||
id: 2,
|
||||
username: 'foo',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
username: 'bar',
|
||||
},
|
||||
]
|
||||
component.form.get('owner').patchValue(2)
|
||||
fixture.detectChanges()
|
||||
expect(changeSpy).toHaveBeenCalledWith({
|
||||
owner: 2,
|
||||
set_permissions: {
|
||||
view: { users: [], groups: [] },
|
||||
change: { users: [], groups: [] },
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,59 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { PermissionsGroupComponent } from './permissions-group.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { of } from 'rxjs'
|
||||
|
||||
describe('PermissionsGroupComponent', () => {
|
||||
let component: PermissionsGroupComponent
|
||||
let fixture: ComponentFixture<PermissionsGroupComponent>
|
||||
let groupService: GroupService
|
||||
let groupServiceSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PermissionsGroupComponent],
|
||||
providers: [GroupService],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientTestingModule,
|
||||
NgSelectModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
groupService = TestBed.inject(GroupService)
|
||||
groupServiceSpy = jest.spyOn(groupService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
count: 2,
|
||||
all: [2, 3],
|
||||
results: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'Group 2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Group 3',
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
fixture = TestBed.createComponent(PermissionsGroupComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should get groups, support use of select', () => {
|
||||
component.writeValue({ id: 2, name: 'Group 2' })
|
||||
expect(component.value).toEqual({ id: 2, name: 'Group 2' })
|
||||
expect(groupServiceSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,60 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { PermissionsUserComponent } from './permissions-user.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
import { of } from 'rxjs'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
|
||||
describe('PermissionsUserComponent', () => {
|
||||
let component: PermissionsUserComponent
|
||||
let fixture: ComponentFixture<PermissionsUserComponent>
|
||||
let userService: UserService
|
||||
let userServiceSpy
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PermissionsUserComponent],
|
||||
providers: [UserService],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
HttpClientTestingModule,
|
||||
NgSelectModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
userService = TestBed.inject(UserService)
|
||||
userServiceSpy = jest.spyOn(userService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
count: 2,
|
||||
all: [2, 3],
|
||||
results: [
|
||||
{
|
||||
id: 2,
|
||||
name: 'User 2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'User 3',
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
fixture = TestBed.createComponent(PermissionsUserComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should get users, support use of select', () => {
|
||||
component.writeValue({ id: 2, name: 'User 2' })
|
||||
expect(component.value).toEqual({ id: 2, name: 'User 2' })
|
||||
expect(userServiceSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -26,7 +26,7 @@
|
||||
<use xlink:href="assets/bootstrap-icons.svg#plus" />
|
||||
</svg>
|
||||
</button>
|
||||
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" i18n-title title="Filter documents with this {{title}}">
|
||||
<button *ngIf="showFilter" class="btn btn-outline-secondary" type="button" (click)="onFilterDocuments()" [disabled]="isPrivate || this.value === null" title="{{ filterButtonTitle }}">
|
||||
<svg class="buttonicon" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#filter" />
|
||||
</svg>
|
||||
|
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { SelectComponent } from './select.component'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
} from 'src/app/data/matching-model'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
|
||||
const items: PaperlessTag[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Tag1',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Tag2',
|
||||
is_inbox_tag: true,
|
||||
matching_algorithm: MATCH_ALL,
|
||||
match: 'str',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Tag10',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||
},
|
||||
]
|
||||
|
||||
describe('SelectComponent', () => {
|
||||
let component: SelectComponent
|
||||
let fixture: ComponentFixture<SelectComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SelectComponent],
|
||||
providers: [],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
RouterTestingModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(SelectComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support private items', () => {
|
||||
component.value = 3
|
||||
component.items = items
|
||||
expect(component.items).toContainEqual({
|
||||
id: 3,
|
||||
name: 'Private',
|
||||
private: true,
|
||||
})
|
||||
|
||||
component.checkForPrivateItems([4, 5])
|
||||
expect(component.items).toContainEqual({
|
||||
id: 4,
|
||||
name: 'Private',
|
||||
private: true,
|
||||
})
|
||||
expect(component.items).toContainEqual({
|
||||
id: 5,
|
||||
name: 'Private',
|
||||
private: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('should support suggestions', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
component.items = items
|
||||
component.suggestions = [1, 2]
|
||||
fixture.detectChanges()
|
||||
const suggestionAnchor: HTMLAnchorElement =
|
||||
fixture.nativeElement.querySelector('a')
|
||||
suggestionAnchor.click()
|
||||
expect(component.value).toEqual(1)
|
||||
})
|
||||
|
||||
it('should support create new and emit the value', () => {
|
||||
expect(component.allowCreateNew).toBeFalsy()
|
||||
component.items = items
|
||||
let createNewVal
|
||||
component.createNew.subscribe((v) => (createNewVal = v))
|
||||
expect(component.allowCreateNew).toBeTruthy()
|
||||
component.onSearch({ term: 'foo' })
|
||||
component.addItem(undefined)
|
||||
expect(createNewVal).toEqual('foo')
|
||||
component.addItem('bar')
|
||||
expect(createNewVal).toEqual('bar')
|
||||
component.onSearch({ term: 'baz' })
|
||||
component.clickNew()
|
||||
expect(createNewVal).toEqual('baz')
|
||||
})
|
||||
|
||||
it('should clear search term on blur after delay', fakeAsync(() => {
|
||||
const clearSpy = jest.spyOn(component, 'clearLastSearchTerm')
|
||||
component.onBlur()
|
||||
tick(3000)
|
||||
expect(clearSpy).toHaveBeenCalled()
|
||||
}))
|
||||
})
|
@@ -144,4 +144,8 @@ export class SelectComponent extends AbstractInputComponent<number> {
|
||||
onFilterDocuments() {
|
||||
this.filterDocuments.emit([this.items.find((i) => i.id === this.value)])
|
||||
}
|
||||
|
||||
get filterButtonTitle() {
|
||||
return $localize`Filter documents with this ${this.title}`
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,140 @@
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { TagsComponent } from './tags.component'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import {
|
||||
DEFAULT_MATCHING_ALGORITHM,
|
||||
MATCH_ALL,
|
||||
} from 'src/app/data/matching-model'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { of } from 'rxjs'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import {
|
||||
NgbModal,
|
||||
NgbModalModule,
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
const tags: PaperlessTag[] = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Tag1',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Tag2',
|
||||
is_inbox_tag: true,
|
||||
matching_algorithm: MATCH_ALL,
|
||||
match: 'str',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
name: 'Tag10',
|
||||
is_inbox_tag: false,
|
||||
matching_algorithm: DEFAULT_MATCHING_ALGORITHM,
|
||||
},
|
||||
]
|
||||
|
||||
describe('TagsComponent', () => {
|
||||
let component: TagsComponent
|
||||
let fixture: ComponentFixture<TagsComponent>
|
||||
let input: HTMLInputElement
|
||||
let modalService: NgbModal
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TagsComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: TagService,
|
||||
useValue: {
|
||||
listAll: () => of(tags),
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgSelectModule,
|
||||
RouterTestingModule,
|
||||
HttpClientTestingModule,
|
||||
NgbModalModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
fixture = TestBed.createComponent(TagsComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
|
||||
window.PointerEvent = MouseEvent as any
|
||||
})
|
||||
|
||||
it('should support suggestions', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
component.value = []
|
||||
component.tags = tags
|
||||
component.suggestions = [1, 2]
|
||||
fixture.detectChanges()
|
||||
const suggestionAnchor: HTMLAnchorElement =
|
||||
fixture.nativeElement.querySelector('a')
|
||||
suggestionAnchor.click()
|
||||
expect(component.value).toEqual([1])
|
||||
})
|
||||
|
||||
it('should support create new and open a modal', () => {
|
||||
let activeInstances: NgbModalRef[]
|
||||
modalService.activeInstances.subscribe((v) => (activeInstances = v))
|
||||
component.createTag('foo')
|
||||
expect(modalService.hasOpenModals()).toBeTruthy()
|
||||
expect(activeInstances[0].componentInstance.object.name).toEqual('foo')
|
||||
})
|
||||
|
||||
it('should support create new using last search term and open a modal', () => {
|
||||
let activeInstances: NgbModalRef[]
|
||||
modalService.activeInstances.subscribe((v) => (activeInstances = v))
|
||||
component.onSearch({ term: 'bar' })
|
||||
component.createTag()
|
||||
expect(modalService.hasOpenModals()).toBeTruthy()
|
||||
expect(activeInstances[0].componentInstance.object.name).toEqual('bar')
|
||||
})
|
||||
|
||||
it('should clear search term on blur after delay', fakeAsync(() => {
|
||||
const clearSpy = jest.spyOn(component, 'clearLastSearchTerm')
|
||||
component.onBlur()
|
||||
tick(3000)
|
||||
expect(clearSpy).toHaveBeenCalled()
|
||||
}))
|
||||
|
||||
it('support remove tags', () => {
|
||||
component.tags = tags
|
||||
component.value = [1, 2]
|
||||
component.removeTag(new PointerEvent('point'), 2)
|
||||
expect(component.value).toEqual([1])
|
||||
|
||||
component.disabled = true
|
||||
component.removeTag(new PointerEvent('point'), 1)
|
||||
expect(component.value).toEqual([1])
|
||||
})
|
||||
|
||||
it('should get tags', () => {
|
||||
expect(component.getTag(2)).toBeNull()
|
||||
component.tags = tags
|
||||
expect(component.getTag(2)).toEqual(tags[1])
|
||||
expect(component.getTag(4)).toBeUndefined()
|
||||
})
|
||||
})
|
@@ -11,6 +11,7 @@ import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { TagEditDialogComponent } from '../../edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { EditDialogMode } from '../../edit-dialog/edit-dialog.component'
|
||||
|
||||
@Component({
|
||||
providers: [
|
||||
@@ -105,7 +106,7 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
|
||||
var modal = this.modalService.open(TagEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.dialogMode = 'create'
|
||||
modal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||
if (name) modal.componentInstance.object = { name: name }
|
||||
else if (this._lastSearchTerm)
|
||||
modal.componentInstance.object = { name: this._lastSearchTerm }
|
||||
|
@@ -0,0 +1,36 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import {
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
} from '@angular/forms'
|
||||
import { TextComponent } from './text.component'
|
||||
|
||||
describe('TextComponent', () => {
|
||||
let component: TextComponent
|
||||
let fixture: ComponentFixture<TextComponent>
|
||||
let input: HTMLInputElement
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TextComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(TextComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
input = component.inputField.nativeElement
|
||||
})
|
||||
|
||||
it('should support use of input field', () => {
|
||||
expect(component.value).toBeUndefined()
|
||||
// TODO: why doesnt this work?
|
||||
// input.value = 'foo'
|
||||
// input.dispatchEvent(new Event('change'))
|
||||
// fixture.detectChanges()
|
||||
// expect(component.value).toEqual('foo')
|
||||
})
|
||||
})
|
@@ -0,0 +1,36 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { Title } from '@angular/platform-browser'
|
||||
import { PageHeaderComponent } from './page-header.component'
|
||||
import { environment } from 'src/environments/environment'
|
||||
|
||||
describe('PageHeaderComponent', () => {
|
||||
let component: PageHeaderComponent
|
||||
let fixture: ComponentFixture<PageHeaderComponent>
|
||||
let titleService: Title
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PageHeaderComponent],
|
||||
providers: [],
|
||||
imports: [],
|
||||
}).compileComponents()
|
||||
|
||||
titleService = TestBed.inject(Title)
|
||||
fixture = TestBed.createComponent(PageHeaderComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should display title + subtitle', () => {
|
||||
component.title = 'Foo'
|
||||
component.subTitle = 'Bar'
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).toContain('FooBar')
|
||||
})
|
||||
|
||||
it('should set html title', () => {
|
||||
const titleSpy = jest.spyOn(titleService, 'setTitle')
|
||||
component.title = 'Foo Bar'
|
||||
expect(titleSpy).toHaveBeenCalledWith(`Foo Bar - ${environment.appTitle}`)
|
||||
})
|
||||
})
|
@@ -0,0 +1,90 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { PermissionsDialogComponent } from './permissions-dialog.component'
|
||||
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { of } from 'rxjs'
|
||||
import { PermissionsFormComponent } from '../input/permissions/permissions-form/permissions-form.component'
|
||||
import { SelectComponent } from '../input/select/select.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { PermissionsUserComponent } from '../input/permissions/permissions-user/permissions-user.component'
|
||||
import { PermissionsGroupComponent } from '../input/permissions/permissions-group/permissions-group.component'
|
||||
|
||||
const set_permissions = {
|
||||
owner: 10,
|
||||
set_permissions: {
|
||||
view: {
|
||||
users: [1],
|
||||
groups: [],
|
||||
},
|
||||
edit: {
|
||||
users: [1],
|
||||
groups: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
describe('PermissionsDialogComponent', () => {
|
||||
let component: PermissionsDialogComponent
|
||||
let fixture: ComponentFixture<PermissionsDialogComponent>
|
||||
let modal: NgbActiveModal
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
PermissionsDialogComponent,
|
||||
SafeHtmlPipe,
|
||||
SelectComponent,
|
||||
PermissionsFormComponent,
|
||||
PermissionsUserComponent,
|
||||
PermissionsGroupComponent,
|
||||
],
|
||||
providers: [
|
||||
NgbActiveModal,
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
username: 'user10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
modal = TestBed.inject(NgbActiveModal)
|
||||
fixture = TestBed.createComponent(PermissionsDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should return permissions', () => {
|
||||
component.form.get('permissions_form').setValue(set_permissions)
|
||||
expect(component.permissions).toEqual(set_permissions)
|
||||
})
|
||||
|
||||
it('should close modal on cancel', () => {
|
||||
const closeSpy = jest.spyOn(modal, 'close')
|
||||
component.cancelClicked()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,157 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { of } from 'rxjs'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import {
|
||||
OwnerFilterType,
|
||||
PermissionsFilterDropdownComponent,
|
||||
PermissionsSelectionModel,
|
||||
} from './permissions-filter-dropdown.component'
|
||||
import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.component'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
|
||||
const currentUserID = 13
|
||||
|
||||
describe('PermissionsFilterDropdownComponent', () => {
|
||||
let component: PermissionsFilterDropdownComponent
|
||||
let fixture: ComponentFixture<PermissionsFilterDropdownComponent>
|
||||
let ownerFilterSetResult: PermissionsSelectionModel
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
PermissionsFilterDropdownComponent,
|
||||
ClearableBadgeComponent,
|
||||
IfPermissionsDirective,
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
username: 'user10',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PermissionsService,
|
||||
useValue: {
|
||||
currentUserCan: () => true,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SettingsService,
|
||||
useValue: {
|
||||
currentUser: {
|
||||
id: currentUserID,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(PermissionsFilterDropdownComponent)
|
||||
component = fixture.componentInstance
|
||||
component.ownerFilterSet.subscribe(
|
||||
(model) => (ownerFilterSetResult = model)
|
||||
)
|
||||
component.selectionModel = new PermissionsSelectionModel()
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should report is active', () => {
|
||||
component.setFilter(OwnerFilterType.NONE)
|
||||
expect(component.isActive).toBeFalsy()
|
||||
component.setFilter(OwnerFilterType.OTHERS)
|
||||
expect(component.isActive).toBeTruthy()
|
||||
component.setFilter(OwnerFilterType.NONE)
|
||||
component.selectionModel.hideUnowned = true
|
||||
expect(component.isActive).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support reset', () => {
|
||||
component.setFilter(OwnerFilterType.OTHERS)
|
||||
expect(component.selectionModel.ownerFilter).not.toEqual(
|
||||
OwnerFilterType.NONE
|
||||
)
|
||||
component.reset()
|
||||
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.NONE)
|
||||
})
|
||||
|
||||
it('should toggle owner filter type when users selected', () => {
|
||||
component.selectionModel.ownerFilter = OwnerFilterType.NONE
|
||||
|
||||
// this would normally be done by select component
|
||||
component.selectionModel.includeUsers = [12]
|
||||
component.onUserSelect()
|
||||
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.OTHERS)
|
||||
|
||||
// this would normally be done by select component
|
||||
component.selectionModel.includeUsers = null
|
||||
component.onUserSelect()
|
||||
|
||||
expect(component.selectionModel.ownerFilter).toEqual(OwnerFilterType.NONE)
|
||||
})
|
||||
it('should emit a selection model depending on the type of owner filter set', () => {
|
||||
component.selectionModel.ownerFilter = OwnerFilterType.NONE
|
||||
|
||||
component.setFilter(OwnerFilterType.SELF)
|
||||
expect(ownerFilterSetResult).toEqual({
|
||||
excludeUsers: [],
|
||||
hideUnowned: false,
|
||||
includeUsers: [],
|
||||
ownerFilter: OwnerFilterType.SELF,
|
||||
userID: currentUserID,
|
||||
})
|
||||
|
||||
component.setFilter(OwnerFilterType.NOT_SELF)
|
||||
expect(ownerFilterSetResult).toEqual({
|
||||
excludeUsers: [currentUserID],
|
||||
hideUnowned: false,
|
||||
includeUsers: [],
|
||||
ownerFilter: OwnerFilterType.NOT_SELF,
|
||||
userID: null,
|
||||
})
|
||||
|
||||
component.setFilter(OwnerFilterType.NONE)
|
||||
expect(ownerFilterSetResult).toEqual({
|
||||
excludeUsers: [],
|
||||
hideUnowned: false,
|
||||
includeUsers: [],
|
||||
ownerFilter: OwnerFilterType.NONE,
|
||||
userID: null,
|
||||
})
|
||||
|
||||
component.setFilter(OwnerFilterType.UNOWNED)
|
||||
expect(ownerFilterSetResult).toEqual({
|
||||
excludeUsers: [],
|
||||
hideUnowned: false,
|
||||
includeUsers: [],
|
||||
ownerFilter: OwnerFilterType.UNOWNED,
|
||||
userID: null,
|
||||
})
|
||||
})
|
||||
})
|
@@ -0,0 +1,96 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { PermissionsSelectComponent } from './permissions-select.component'
|
||||
import {
|
||||
FormsModule,
|
||||
NG_VALUE_ACCESSOR,
|
||||
ReactiveFormsModule,
|
||||
} from '@angular/forms'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import {
|
||||
PermissionAction,
|
||||
PermissionType,
|
||||
} from 'src/app/services/permissions.service'
|
||||
import { By } from '@angular/platform-browser'
|
||||
|
||||
const permissions = [
|
||||
'add_document',
|
||||
'view_document',
|
||||
'change_document',
|
||||
'delete_document',
|
||||
'change_tag',
|
||||
'view_documenttype',
|
||||
]
|
||||
|
||||
const inheritedPermissions = ['change_tag', 'view_documenttype']
|
||||
|
||||
describe('PermissionsSelectComponent', () => {
|
||||
let component: PermissionsSelectComponent
|
||||
let fixture: ComponentFixture<PermissionsSelectComponent>
|
||||
let permissionsChangeResult: Permissions
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [PermissionsSelectComponent],
|
||||
providers: [],
|
||||
imports: [FormsModule, ReactiveFormsModule, NgbModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(PermissionsSelectComponent)
|
||||
fixture.debugElement.injector.get(NG_VALUE_ACCESSOR)
|
||||
component = fixture.componentInstance
|
||||
component.registerOnChange((r) => (permissionsChangeResult = r))
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should create controls for all PermissionType and PermissionAction', () => {
|
||||
expect(Object.values(component.form.controls)).toHaveLength(
|
||||
Object.keys(PermissionType).length
|
||||
)
|
||||
for (var type in component.form.controls) {
|
||||
expect(
|
||||
Object.values(component.form.controls[type].controls)
|
||||
).toHaveLength(Object.keys(PermissionAction).length)
|
||||
}
|
||||
// coverage
|
||||
component.registerOnTouched(() => {})
|
||||
component.setDisabledState(true)
|
||||
})
|
||||
|
||||
it('should allow toggle all on / off', () => {
|
||||
component.ngOnInit()
|
||||
expect(component.typesWithAllActions.values).toHaveLength(0)
|
||||
component.toggleAll({ target: { checked: true } }, 'Tag')
|
||||
expect(component.typesWithAllActions).toContain('Tag')
|
||||
component.toggleAll({ target: { checked: false } }, 'Tag')
|
||||
expect(component.typesWithAllActions.values).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should update on permissions set', () => {
|
||||
component.ngOnInit()
|
||||
component.writeValue(permissions)
|
||||
expect(permissionsChangeResult).toEqual(permissions)
|
||||
expect(component.typesWithAllActions).toContain('Document')
|
||||
})
|
||||
|
||||
it('should update checkboxes on permissions set', () => {
|
||||
component.ngOnInit()
|
||||
component.writeValue(permissions)
|
||||
fixture.detectChanges()
|
||||
const input1 = fixture.debugElement.query(By.css('input#Document_Add'))
|
||||
expect(input1.nativeElement.checked).toBeTruthy()
|
||||
const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
|
||||
expect(input2.nativeElement.checked).toBeTruthy()
|
||||
})
|
||||
|
||||
it('disable checkboxes when permissions are inherited', () => {
|
||||
component.ngOnInit()
|
||||
component.inheritedPermissions = inheritedPermissions
|
||||
expect(component.isInherited('Document', 'Add')).toBeFalsy()
|
||||
expect(component.isInherited('Document')).toBeFalsy()
|
||||
expect(component.isInherited('Tag', 'Change')).toBeTruthy()
|
||||
const input1 = fixture.debugElement.query(By.css('input#Document_Add'))
|
||||
expect(input1.nativeElement.disabled).toBeFalsy()
|
||||
const input2 = fixture.debugElement.query(By.css('input#Tag_Change'))
|
||||
expect(input2.nativeElement.disabled).toBeTruthy()
|
||||
})
|
||||
})
|
@@ -0,0 +1,31 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { SelectComponent } from '../input/select/select.component'
|
||||
import { SelectDialogComponent } from './select-dialog.component'
|
||||
|
||||
describe('SelectDialogComponent', () => {
|
||||
let component: SelectDialogComponent
|
||||
let fixture: ComponentFixture<SelectDialogComponent>
|
||||
let modal: NgbActiveModal
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [SelectDialogComponent, SelectComponent],
|
||||
providers: [NgbActiveModal],
|
||||
imports: [NgSelectModule, FormsModule, ReactiveFormsModule],
|
||||
}).compileComponents()
|
||||
|
||||
modal = TestBed.inject(NgbActiveModal)
|
||||
fixture = TestBed.createComponent(SelectDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should close modal on cancel', () => {
|
||||
const closeSpy = jest.spyOn(modal, 'close')
|
||||
component.cancelClicked()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
51
src-ui/src/app/components/common/tag/tag.component.spec.ts
Normal file
51
src-ui/src/app/components/common/tag/tag.component.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { TagComponent } from './tag.component'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { By } from '@angular/platform-browser'
|
||||
|
||||
const tag: PaperlessTag = {
|
||||
id: 1,
|
||||
color: '#ff0000',
|
||||
name: 'Tag1',
|
||||
}
|
||||
|
||||
describe('TagComponent', () => {
|
||||
let component: TagComponent
|
||||
let fixture: ComponentFixture<TagComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [TagComponent],
|
||||
providers: [],
|
||||
imports: [],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(TagComponent)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should create tag with background color', () => {
|
||||
component.tag = tag
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
fixture.debugElement.query(By.css('span')).nativeElement.style
|
||||
.backgroundColor
|
||||
).toEqual('rgb(255, 0, 0)')
|
||||
})
|
||||
|
||||
it('should handle private tags', () => {
|
||||
expect(
|
||||
fixture.debugElement.query(By.css('span')).nativeElement.textContent
|
||||
).toEqual('Private')
|
||||
})
|
||||
|
||||
it('should support clickable option', () => {
|
||||
component.tag = tag
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.query(By.css('a.badge'))).toBeNull()
|
||||
component.clickable = true
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.query(By.css('a.badge'))).not.toBeNull()
|
||||
})
|
||||
})
|
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
TestBed,
|
||||
discardPeriodicTasks,
|
||||
fakeAsync,
|
||||
flush,
|
||||
} from '@angular/core/testing'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ToastsComponent } from './toasts.component'
|
||||
import { ComponentFixture } from '@angular/core/testing'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { of } from 'rxjs'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
describe('ToastsComponent', () => {
|
||||
let component: ToastsComponent
|
||||
let fixture: ComponentFixture<ToastsComponent>
|
||||
let toastService: ToastService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [ToastsComponent],
|
||||
imports: [HttpClientTestingModule, NgbModule],
|
||||
providers: [
|
||||
{
|
||||
provide: ToastService,
|
||||
useValue: {
|
||||
getToasts: () =>
|
||||
of([
|
||||
{
|
||||
title: 'Title',
|
||||
content: 'content',
|
||||
delay: 5000,
|
||||
},
|
||||
{
|
||||
title: 'Error',
|
||||
content: 'Error content',
|
||||
delay: 5000,
|
||||
error: new Error('Error message'),
|
||||
},
|
||||
]),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(ToastsComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
toastService = TestBed.inject(ToastService)
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should call getToasts and return toasts', fakeAsync(() => {
|
||||
const spy = jest.spyOn(toastService, 'getToasts').mockReset()
|
||||
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
|
||||
expect(spy).toHaveBeenCalled()
|
||||
expect(component.toasts).toContainEqual({
|
||||
title: 'Title',
|
||||
content: 'content',
|
||||
delay: 5000,
|
||||
})
|
||||
|
||||
component.ngOnDestroy()
|
||||
flush()
|
||||
discardPeriodicTasks()
|
||||
}))
|
||||
|
||||
it('should show a toast', fakeAsync(() => {
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
|
||||
expect(fixture.nativeElement.textContent).toContain('Title')
|
||||
|
||||
component.ngOnDestroy()
|
||||
flush()
|
||||
discardPeriodicTasks()
|
||||
}))
|
||||
|
||||
it('should show an error if given with toast', fakeAsync(() => {
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
|
||||
expect(fixture.nativeElement.querySelector('details')).not.toBeNull()
|
||||
expect(fixture.nativeElement.textContent).toContain('Error message')
|
||||
|
||||
component.ngOnDestroy()
|
||||
flush()
|
||||
discardPeriodicTasks()
|
||||
}))
|
||||
})
|
@@ -15,7 +15,7 @@ export class ToastsComponent implements OnInit, OnDestroy {
|
||||
toasts: Toast[] = []
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subscription.unsubscribe()
|
||||
this.subscription?.unsubscribe()
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
117
src-ui/src/app/components/dashboard/dashboard.component.spec.ts
Normal file
117
src-ui/src/app/components/dashboard/dashboard.component.spec.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { NgbAlertModule, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { DashboardComponent } from './dashboard.component'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { StatisticsWidgetComponent } from './widgets/statistics-widget/statistics-widget.component'
|
||||
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
||||
import { WidgetFrameComponent } from './widgets/widget-frame/widget-frame.component'
|
||||
import { UploadFileWidgetComponent } from './widgets/upload-file-widget/upload-file-widget.component'
|
||||
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { SavedViewWidgetComponent } from './widgets/saved-view-widget/saved-view-widget.component'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { NgxFileDropModule } from 'ngx-file-drop'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { TourNgBootstrapModule, TourService } from 'ngx-ui-tour-ng-bootstrap'
|
||||
|
||||
describe('DashboardComponent', () => {
|
||||
let component: DashboardComponent
|
||||
let fixture: ComponentFixture<DashboardComponent>
|
||||
let settingsService: SettingsService
|
||||
let tourService: TourService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DashboardComponent,
|
||||
StatisticsWidgetComponent,
|
||||
PageHeaderComponent,
|
||||
WidgetFrameComponent,
|
||||
UploadFileWidgetComponent,
|
||||
IfPermissionsDirective,
|
||||
SavedViewWidgetComponent,
|
||||
],
|
||||
providers: [
|
||||
PermissionsGuard,
|
||||
{
|
||||
provide: PermissionsService,
|
||||
useValue: {
|
||||
currentUserCan: () => true,
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SavedViewService,
|
||||
useValue: {
|
||||
dashboardViews: [
|
||||
{
|
||||
id: 1,
|
||||
name: 'saved view 1',
|
||||
show_on_dashboard: true,
|
||||
sort_field: 'added',
|
||||
sort_reverse: true,
|
||||
filter_rules: [],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'saved view 2',
|
||||
show_on_dashboard: true,
|
||||
sort_field: 'created',
|
||||
sort_reverse: true,
|
||||
filter_rules: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
NgbAlertModule,
|
||||
HttpClientTestingModule,
|
||||
NgxFileDropModule,
|
||||
RouterTestingModule,
|
||||
TourNgBootstrapModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = {
|
||||
first_name: 'Foo',
|
||||
last_name: 'Bar',
|
||||
}
|
||||
tourService = TestBed.inject(TourService)
|
||||
fixture = TestBed.createComponent(DashboardComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should show a welcome message', () => {
|
||||
expect(component.subtitle).toEqual(`Hello Foo, welcome to Paperless-ngx`)
|
||||
settingsService.currentUser = {
|
||||
id: 1,
|
||||
}
|
||||
expect(component.subtitle).toEqual(`Welcome to Paperless-ngx`)
|
||||
})
|
||||
|
||||
it('should show dashboard widgets', () => {
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.directive(SavedViewWidgetComponent))
|
||||
).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should end tour service if still running and welcome widget dismissed', () => {
|
||||
jest.spyOn(tourService, 'getStatus').mockReturnValueOnce(1)
|
||||
const endSpy = jest.spyOn(tourService, 'end')
|
||||
component.completeTour()
|
||||
expect(endSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should save tour completion if it was stopped and welcome widget dismissed', () => {
|
||||
jest.spyOn(tourService, 'getStatus').mockReturnValueOnce(0)
|
||||
const settingsCompleteTourSpy = jest.spyOn(settingsService, 'completeTour')
|
||||
component.completeTour()
|
||||
expect(settingsCompleteTourSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
@@ -0,0 +1,165 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { Router } from '@angular/router'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { of, Subject } from 'rxjs'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { FILTER_HAS_TAGS_ALL } from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
import { SavedViewWidgetComponent } from './saved-view-widget.component'
|
||||
|
||||
const savedView: PaperlessSavedView = {
|
||||
id: 1,
|
||||
name: 'Saved View 1',
|
||||
sort_field: 'added',
|
||||
sort_reverse: true,
|
||||
show_in_sidebar: true,
|
||||
show_on_dashboard: true,
|
||||
filter_rules: [
|
||||
{
|
||||
rule_type: FILTER_HAS_TAGS_ALL,
|
||||
value: '1,2',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const documentResults = [
|
||||
{
|
||||
id: 2,
|
||||
title: 'doc2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: 'doc3',
|
||||
},
|
||||
]
|
||||
|
||||
describe('SavedViewWidgetComponent', () => {
|
||||
let component: SavedViewWidgetComponent
|
||||
let fixture: ComponentFixture<SavedViewWidgetComponent>
|
||||
let documentService: DocumentService
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let documentListViewService: DocumentListViewService
|
||||
let router: Router
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
SavedViewWidgetComponent,
|
||||
WidgetFrameComponent,
|
||||
IfPermissionsDirective,
|
||||
CustomDatePipe,
|
||||
DocumentTitlePipe,
|
||||
],
|
||||
providers: [
|
||||
PermissionsGuard,
|
||||
DocumentService,
|
||||
{
|
||||
provide: PermissionsService,
|
||||
useValue: {
|
||||
currentUserCan: () => true,
|
||||
},
|
||||
},
|
||||
CustomDatePipe,
|
||||
DatePipe,
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgbModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
documentListViewService = TestBed.inject(DocumentListViewService)
|
||||
router = TestBed.inject(Router)
|
||||
fixture = TestBed.createComponent(SavedViewWidgetComponent)
|
||||
component = fixture.componentInstance
|
||||
component.savedView = savedView
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should show a list of documents', () => {
|
||||
jest.spyOn(documentService, 'listFiltered').mockReturnValue(
|
||||
of({
|
||||
all: [2, 3],
|
||||
count: 2,
|
||||
results: documentResults,
|
||||
})
|
||||
)
|
||||
component.ngOnInit()
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain('doc2')
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain('doc3')
|
||||
})
|
||||
|
||||
it('should call api endpoint and load results', () => {
|
||||
const listAllSpy = jest.spyOn(documentService, 'listFiltered')
|
||||
listAllSpy.mockReturnValue(
|
||||
of({
|
||||
all: [2, 3],
|
||||
count: 2,
|
||||
results: documentResults,
|
||||
})
|
||||
)
|
||||
component.ngOnInit()
|
||||
expect(listAllSpy).toHaveBeenCalledWith(
|
||||
1,
|
||||
10,
|
||||
savedView.sort_field,
|
||||
savedView.sort_reverse,
|
||||
savedView.filter_rules,
|
||||
{
|
||||
truncate_content: true,
|
||||
}
|
||||
)
|
||||
fixture.detectChanges()
|
||||
expect(component.documents).toEqual(documentResults)
|
||||
})
|
||||
|
||||
it('should reload on document consumption finished', () => {
|
||||
const fileStatusSubject = new Subject<FileStatus>()
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'onDocumentConsumptionFinished')
|
||||
.mockReturnValue(fileStatusSubject)
|
||||
const reloadSpy = jest.spyOn(component, 'reload')
|
||||
component.ngOnInit()
|
||||
fileStatusSubject.next(new FileStatus())
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should navigate on showAll', () => {
|
||||
const routerSpy = jest.spyOn(router, 'navigate')
|
||||
component.showAll()
|
||||
expect(routerSpy).toHaveBeenCalledWith(['view', savedView.id])
|
||||
savedView.show_in_sidebar = false
|
||||
component.showAll()
|
||||
expect(routerSpy).toHaveBeenCalledWith(['documents'], {
|
||||
queryParams: { view: savedView.id },
|
||||
})
|
||||
})
|
||||
|
||||
it('should navigate via quickfilter on click tag', () => {
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.clickTag({ id: 11, name: 'Tag11' }, new MouseEvent('click'))
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{ rule_type: FILTER_HAS_TAGS_ALL, value: '11' },
|
||||
])
|
||||
})
|
||||
})
|
@@ -0,0 +1,110 @@
|
||||
import { TestBed } from '@angular/core/testing'
|
||||
import { StatisticsWidgetComponent } from './statistics-widget.component'
|
||||
import { ComponentFixture } from '@angular/core/testing'
|
||||
import {
|
||||
HttpClientTestingModule,
|
||||
HttpTestingController,
|
||||
} from '@angular/common/http/testing'
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
|
||||
describe('StatisticsWidgetComponent', () => {
|
||||
let component: StatisticsWidgetComponent
|
||||
let fixture: ComponentFixture<StatisticsWidgetComponent>
|
||||
let httpTestingController: HttpTestingController
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [StatisticsWidgetComponent, WidgetFrameComponent],
|
||||
providers: [PermissionsGuard],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgbModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(StatisticsWidgetComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should call api statistics endpoint', () => {
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}statistics/`
|
||||
)
|
||||
expect(req.request.method).toEqual('GET')
|
||||
})
|
||||
|
||||
it('should display inbox link with count', () => {
|
||||
const mockStats = {
|
||||
documents_total: 200,
|
||||
documents_inbox: 18,
|
||||
inbox_tag: 10,
|
||||
}
|
||||
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}statistics/`
|
||||
)
|
||||
|
||||
req.flush(mockStats)
|
||||
fixture.detectChanges()
|
||||
|
||||
const goToInboxSpy = jest.spyOn(component, 'goToInbox')
|
||||
|
||||
expect(fixture.nativeElement.textContent.replace(/\s/g, '')).toContain(
|
||||
'inbox:18'
|
||||
)
|
||||
const link = fixture.nativeElement.querySelector('a') as HTMLAnchorElement
|
||||
expect(link).not.toBeNull()
|
||||
link.click()
|
||||
expect(goToInboxSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should display mime types with counts', () => {
|
||||
const mockStats = {
|
||||
documents_total: 200,
|
||||
documents_inbox: 18,
|
||||
inbox_tag: 10,
|
||||
document_file_type_counts: [
|
||||
{
|
||||
mime_type: 'application/pdf',
|
||||
mime_type_count: 160,
|
||||
},
|
||||
{
|
||||
mime_type: 'text/plain',
|
||||
mime_type_count: 20,
|
||||
},
|
||||
{
|
||||
mime_type: 'text/csv',
|
||||
mime_type_count: 20,
|
||||
},
|
||||
],
|
||||
character_count: 162312,
|
||||
}
|
||||
|
||||
const req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}statistics/`
|
||||
)
|
||||
|
||||
req.flush(mockStats)
|
||||
fixture.detectChanges()
|
||||
|
||||
expect(fixture.nativeElement.textContent.replace(/\s/g, '')).toContain(
|
||||
'PDF(80%)'
|
||||
)
|
||||
expect(fixture.nativeElement.textContent.replace(/\s/g, '')).toContain(
|
||||
'TXT(10%)'
|
||||
)
|
||||
expect(fixture.nativeElement.textContent.replace(/\s/g, '')).toContain(
|
||||
'CSV(10%)'
|
||||
)
|
||||
})
|
||||
})
|
@@ -0,0 +1,173 @@
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import {
|
||||
NgbModule,
|
||||
NgbAlertModule,
|
||||
NgbAlert,
|
||||
NgbCollapse,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxFileDropModule } from 'ngx-file-drop'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import {
|
||||
ConsumerStatusService,
|
||||
FileStatus,
|
||||
FileStatusPhase,
|
||||
} from 'src/app/services/consumer-status.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { UploadDocumentsService } from 'src/app/services/upload-documents.service'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
import { UploadFileWidgetComponent } from './upload-file-widget.component'
|
||||
|
||||
describe('UploadFileWidgetComponent', () => {
|
||||
let component: UploadFileWidgetComponent
|
||||
let fixture: ComponentFixture<UploadFileWidgetComponent>
|
||||
let consumerStatusService: ConsumerStatusService
|
||||
let uploadDocumentsService: UploadDocumentsService
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
UploadFileWidgetComponent,
|
||||
WidgetFrameComponent,
|
||||
IfPermissionsDirective,
|
||||
],
|
||||
providers: [
|
||||
PermissionsGuard,
|
||||
{
|
||||
provide: PermissionsService,
|
||||
useValue: {
|
||||
currentUserCan: () => true,
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
NgbModule,
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
NgxFileDropModule,
|
||||
NgbAlertModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
consumerStatusService = TestBed.inject(ConsumerStatusService)
|
||||
uploadDocumentsService = TestBed.inject(UploadDocumentsService)
|
||||
fixture = TestBed.createComponent(UploadFileWidgetComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should support drop files', () => {
|
||||
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFiles')
|
||||
component.dropped([])
|
||||
expect(uploadSpy).toHaveBeenCalled()
|
||||
// coverage
|
||||
component.fileLeave(null)
|
||||
component.fileOver(null)
|
||||
})
|
||||
|
||||
it('should generate stats summary', () => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
expect(component.getStatusSummary()).toEqual(
|
||||
'Processing: 6, Failed: 1, Added: 4'
|
||||
)
|
||||
})
|
||||
|
||||
it('should report an upload progress summary', () => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
expect(component.getTotalUploadProgress()).toEqual(0.75)
|
||||
})
|
||||
|
||||
it('should change color by status phase', () => {
|
||||
const processingStatus = new FileStatus()
|
||||
processingStatus.phase = FileStatusPhase.PROCESSING
|
||||
expect(component.getStatusColor(processingStatus)).toEqual('primary')
|
||||
const failedStatus = new FileStatus()
|
||||
failedStatus.phase = FileStatusPhase.FAILED
|
||||
expect(component.getStatusColor(failedStatus)).toEqual('danger')
|
||||
const successStatus = new FileStatus()
|
||||
successStatus.phase = FileStatusPhase.SUCCESS
|
||||
expect(component.getStatusColor(successStatus)).toEqual('success')
|
||||
})
|
||||
|
||||
it('should enforce a maximum number of alerts', () => {
|
||||
mockConsumerStatuses(consumerStatusService)
|
||||
fixture.detectChanges()
|
||||
// 5 total, 1 hidden
|
||||
expect(fixture.debugElement.queryAll(By.directive(NgbAlert))).toHaveLength(
|
||||
6
|
||||
)
|
||||
expect(
|
||||
fixture.debugElement
|
||||
.query(By.directive(NgbCollapse))
|
||||
.queryAll(By.directive(NgbAlert))
|
||||
).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('should allow dismissing an alert', () => {
|
||||
const dismissSpy = jest.spyOn(consumerStatusService, 'dismiss')
|
||||
component.dismiss(new FileStatus())
|
||||
expect(dismissSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow dismissing all alerts', () => {
|
||||
const dismissSpy = jest.spyOn(consumerStatusService, 'dismissCompleted')
|
||||
component.dismissCompleted()
|
||||
expect(dismissSpy).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
function mockConsumerStatuses(consumerStatusService) {
|
||||
const partialUpload1 = new FileStatus()
|
||||
partialUpload1.currentPhaseProgress = 50
|
||||
partialUpload1.currentPhaseMaxProgress = 50
|
||||
const partialUpload2 = new FileStatus()
|
||||
partialUpload2.currentPhaseProgress = 25
|
||||
partialUpload2.currentPhaseMaxProgress = 50
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'getConsumerStatus')
|
||||
.mockImplementation((phase) => {
|
||||
switch (phase) {
|
||||
case FileStatusPhase.FAILED:
|
||||
return [new FileStatus()]
|
||||
case FileStatusPhase.PROCESSING:
|
||||
return [new FileStatus(), new FileStatus()]
|
||||
case FileStatusPhase.STARTED:
|
||||
return [new FileStatus(), new FileStatus(), new FileStatus()]
|
||||
case FileStatusPhase.SUCCESS:
|
||||
return [
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
]
|
||||
case FileStatusPhase.UPLOADING:
|
||||
return [partialUpload1, partialUpload2]
|
||||
default:
|
||||
return [
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
]
|
||||
}
|
||||
})
|
||||
jest
|
||||
.spyOn(consumerStatusService, 'getConsumerStatusNotCompleted')
|
||||
.mockImplementation(() => {
|
||||
return [
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
new FileStatus(),
|
||||
]
|
||||
})
|
||||
}
|
@@ -69,9 +69,6 @@ export class UploadFileWidgetComponent extends ComponentWithPermissions {
|
||||
return this.consumerStatusService.getConsumerStatus(FileStatusPhase.SUCCESS)
|
||||
}
|
||||
|
||||
getStatusCompleted() {
|
||||
return this.consumerStatusService.getConsumerStatusCompleted()
|
||||
}
|
||||
getTotalUploadProgress() {
|
||||
let current = 0
|
||||
let max = 0
|
||||
|
@@ -0,0 +1,33 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { NgbAlertModule, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { WidgetFrameComponent } from '../widget-frame/widget-frame.component'
|
||||
import { WelcomeWidgetComponent } from './welcome-widget.component'
|
||||
|
||||
describe('WelcomeWidgetComponent', () => {
|
||||
let component: WelcomeWidgetComponent
|
||||
let fixture: ComponentFixture<WelcomeWidgetComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [WelcomeWidgetComponent, WidgetFrameComponent],
|
||||
providers: [PermissionsGuard],
|
||||
imports: [NgbAlertModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(WelcomeWidgetComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should be dismissable', () => {
|
||||
let dismissResult
|
||||
component.dismiss.subscribe(() => (dismissResult = true))
|
||||
fixture.debugElement
|
||||
.query(By.directive(NgbAlert))
|
||||
.triggerEventHandler('closed')
|
||||
expect(dismissResult).toBeTruthy()
|
||||
})
|
||||
})
|
@@ -0,0 +1,53 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { NgbAlertModule, NgbAlert } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { WidgetFrameComponent } from './widget-frame.component'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<div>
|
||||
<button
|
||||
*appIfObjectPermissions="{
|
||||
object: { id: 2, owner: user1 },
|
||||
action: 'view'
|
||||
}"
|
||||
>
|
||||
Some Text
|
||||
</button>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
class TestComponent extends WidgetFrameComponent {}
|
||||
|
||||
describe('WidgetFrameComponent', () => {
|
||||
let component: WidgetFrameComponent
|
||||
let fixture: ComponentFixture<WidgetFrameComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [WidgetFrameComponent, WidgetFrameComponent],
|
||||
providers: [PermissionsGuard],
|
||||
imports: [NgbAlertModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(WidgetFrameComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should show title', () => {
|
||||
component.title = 'Foo'
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain('Foo')
|
||||
})
|
||||
|
||||
it('should show loading indicator', () => {
|
||||
expect(fixture.debugElement.query(By.css('.spinner-border'))).toBeNull()
|
||||
component.loading = true
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.query(By.css('.spinner-border'))).not.toBeNull()
|
||||
})
|
||||
})
|
@@ -0,0 +1,58 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'
|
||||
import { of } from 'rxjs'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { DocumentAsnComponent } from './document-asn.component'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import { FilterRule } from 'src/app/data/filter-rule'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
|
||||
describe('DocumentAsnComponent', () => {
|
||||
let component: DocumentAsnComponent
|
||||
let fixture: ComponentFixture<DocumentAsnComponent>
|
||||
let router: Router
|
||||
let activatedRoute: ActivatedRoute
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [DocumentAsnComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: DocumentService,
|
||||
useValue: {
|
||||
listAllFilteredIds: (rules: FilterRule[]) =>
|
||||
rules[0].value === '1234' ? of([1]) : of([]),
|
||||
},
|
||||
},
|
||||
PermissionsGuard,
|
||||
],
|
||||
imports: [RouterTestingModule.withRoutes(routes)],
|
||||
}).compileComponents()
|
||||
|
||||
router = TestBed.inject(Router)
|
||||
activatedRoute = TestBed.inject(ActivatedRoute)
|
||||
fixture = TestBed.createComponent(DocumentAsnComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should navigate on valid asn', () => {
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ id: '1234' })))
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
component.ngOnInit()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['documents', 1])
|
||||
})
|
||||
|
||||
it('should 404 on invalid asn', () => {
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ id: '5578' })))
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
component.ngOnInit()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['404'])
|
||||
})
|
||||
})
|
@@ -191,9 +191,12 @@
|
||||
<div [ngbNavOutlet]="nav" class="mt-2"></div>
|
||||
|
||||
<ng-container>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>
|
||||
<button type="button" class="btn btn-outline-primary" (click)="saveEditNext()" *ngIf="hasNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save & next</button>
|
||||
<button type="submit" class="btn btn-primary" *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button>
|
||||
<button type="button" class="btn btn-outline-secondary me-2" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>
|
||||
<ng-container *appIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
|
||||
<button *ngIf="hasNext()" type="button" class="btn btn-outline-primary me-2" (click)="saveEditNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save & next</button>
|
||||
<button *ngIf="!hasNext()" type="button" class="btn btn-outline-primary me-2" (click)="save(true)" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save & close</button>
|
||||
<button type="submit" class="btn btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</form>
|
||||
</div>
|
||||
|
@@ -0,0 +1,816 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
discardPeriodicTasks,
|
||||
} from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { Router, ActivatedRoute, convertToParamMap } from '@angular/router'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import {
|
||||
NgbModal,
|
||||
NgbModule,
|
||||
NgbModalModule,
|
||||
NgbModalRef,
|
||||
NgbDateStruct,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { PdfViewerComponent } from 'ng2-pdf-viewer'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { routes } from 'src/app/app-routing.module'
|
||||
import {
|
||||
FILTER_FULLTEXT_MORELIKE,
|
||||
FILTER_CORRESPONDENT,
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
FILTER_STORAGE_PATH,
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
FILTER_CREATED_AFTER,
|
||||
FILTER_CREATED_BEFORE,
|
||||
} from 'src/app/data/filter-rule-type'
|
||||
import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent'
|
||||
import { PaperlessDocument } from 'src/app/data/paperless-document'
|
||||
import { PaperlessDocumentType } from 'src/app/data/paperless-document-type'
|
||||
import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { OpenDocumentsService } from 'src/app/services/open-documents.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import { DocumentService } from 'src/app/services/rest/document.service'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { ConfirmDialogComponent } from '../common/confirm-dialog/confirm-dialog.component'
|
||||
import { CorrespondentEditDialogComponent } from '../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
|
||||
import { DocumentTypeEditDialogComponent } from '../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
|
||||
import { StoragePathEditDialogComponent } from '../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
|
||||
import { DateComponent } from '../common/input/date/date.component'
|
||||
import { NumberComponent } from '../common/input/number/number.component'
|
||||
import { PermissionsFormComponent } from '../common/input/permissions/permissions-form/permissions-form.component'
|
||||
import { SelectComponent } from '../common/input/select/select.component'
|
||||
import { TagsComponent } from '../common/input/tags/tags.component'
|
||||
import { TextComponent } from '../common/input/text/text.component'
|
||||
import { PageHeaderComponent } from '../common/page-header/page-header.component'
|
||||
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
|
||||
import { DocumentDetailComponent } from './document-detail.component'
|
||||
|
||||
const doc: PaperlessDocument = {
|
||||
id: 3,
|
||||
title: 'Doc 3',
|
||||
correspondent: 11,
|
||||
document_type: 21,
|
||||
storage_path: 31,
|
||||
tags: [41, 42, 43],
|
||||
content: 'text content',
|
||||
added: new Date(),
|
||||
created: new Date(),
|
||||
archive_serial_number: null,
|
||||
original_file_name: 'file.pdf',
|
||||
owner: null,
|
||||
user_can_change: true,
|
||||
notes: [
|
||||
{
|
||||
created: new Date(),
|
||||
note: 'note 1',
|
||||
user: 1,
|
||||
},
|
||||
{
|
||||
created: new Date(),
|
||||
note: 'note 2',
|
||||
user: 2,
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('DocumentDetailComponent', () => {
|
||||
let component: DocumentDetailComponent
|
||||
let fixture: ComponentFixture<DocumentDetailComponent>
|
||||
let router: Router
|
||||
let activatedRoute: ActivatedRoute
|
||||
let documentService: DocumentService
|
||||
let openDocumentsService: OpenDocumentsService
|
||||
let modalService: NgbModal
|
||||
let toastService: ToastService
|
||||
let documentListViewService: DocumentListViewService
|
||||
let settingsService: SettingsService
|
||||
|
||||
let currentUserCan = true
|
||||
let currentUserHasObjectPermissions = true
|
||||
let currentUserOwnsObject = true
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DocumentDetailComponent,
|
||||
DocumentTitlePipe,
|
||||
PageHeaderComponent,
|
||||
IfPermissionsDirective,
|
||||
TagsComponent,
|
||||
SelectComponent,
|
||||
TextComponent,
|
||||
NumberComponent,
|
||||
DateComponent,
|
||||
DocumentNotesComponent,
|
||||
CustomDatePipe,
|
||||
DocumentTypeEditDialogComponent,
|
||||
CorrespondentEditDialogComponent,
|
||||
StoragePathEditDialogComponent,
|
||||
IfOwnerDirective,
|
||||
PermissionsFormComponent,
|
||||
SafeHtmlPipe,
|
||||
ConfirmDialogComponent,
|
||||
PdfViewerComponent,
|
||||
SafeUrlPipe,
|
||||
],
|
||||
providers: [
|
||||
DocumentTitlePipe,
|
||||
{
|
||||
provide: CorrespondentService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 11,
|
||||
name: 'Correspondent11',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DocumentTypeService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 21,
|
||||
name: 'DocumentType21',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: StoragePathService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 31,
|
||||
name: 'StoragePath31',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{
|
||||
id: 1,
|
||||
username: 'user1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
username: 'user2',
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PermissionsService,
|
||||
useValue: {
|
||||
currentUserCan: () => currentUserCan,
|
||||
currentUserHasObjectPermissions: () =>
|
||||
currentUserHasObjectPermissions,
|
||||
currentUserOwnsObject: () => currentUserOwnsObject,
|
||||
},
|
||||
},
|
||||
PermissionsGuard,
|
||||
CustomDatePipe,
|
||||
DatePipe,
|
||||
],
|
||||
imports: [
|
||||
RouterTestingModule.withRoutes(routes),
|
||||
HttpClientTestingModule,
|
||||
NgbModule,
|
||||
NgSelectModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModalModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
router = TestBed.inject(Router)
|
||||
activatedRoute = TestBed.inject(ActivatedRoute)
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ id: 3 })))
|
||||
openDocumentsService = TestBed.inject(OpenDocumentsService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
documentListViewService = TestBed.inject(DocumentListViewService)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
fixture = TestBed.createComponent(DocumentDetailComponent)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
it('should load four tabs via url params', () => {
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ id: 3, section: 'notes' })))
|
||||
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(null)
|
||||
jest
|
||||
.spyOn(openDocumentsService, 'openDocument')
|
||||
.mockReturnValueOnce(of(true))
|
||||
fixture.detectChanges()
|
||||
expect(component.activeNavID).toEqual(5) // DocumentDetailNavIDs.Notes
|
||||
})
|
||||
|
||||
it('should change url on tab switch', () => {
|
||||
initNormally()
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
component.nav.select(5)
|
||||
component.nav.navChange.next({
|
||||
activeId: 1,
|
||||
nextId: 5,
|
||||
preventDefault: () => {},
|
||||
})
|
||||
fixture.detectChanges()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['documents', 3, 'notes'])
|
||||
})
|
||||
|
||||
it('should update title after debounce', fakeAsync(() => {
|
||||
initNormally()
|
||||
component.titleInput.value = 'Foo Bar'
|
||||
component.titleSubject.next('Foo Bar')
|
||||
tick(1000)
|
||||
expect(component.documentForm.get('title').value).toEqual('Foo Bar')
|
||||
discardPeriodicTasks()
|
||||
}))
|
||||
|
||||
it('should update title before doc change if wasnt updated via debounce', fakeAsync(() => {
|
||||
initNormally()
|
||||
component.titleInput.value = 'Foo Bar'
|
||||
component.titleInput.inputField.nativeElement.dispatchEvent(
|
||||
new Event('change')
|
||||
)
|
||||
tick(1000)
|
||||
expect(component.documentForm.get('title').value).toEqual('Foo Bar')
|
||||
}))
|
||||
|
||||
it('should load non-open document via param', () => {
|
||||
initNormally()
|
||||
expect(component.document).toEqual(doc)
|
||||
})
|
||||
|
||||
it('should load already-opened document via param', () => {
|
||||
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
|
||||
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(doc)
|
||||
fixture.detectChanges() // calls ngOnInit
|
||||
expect(component.document).toEqual(doc)
|
||||
})
|
||||
|
||||
it('should disable form if user cannot edit', () => {
|
||||
currentUserHasObjectPermissions = false
|
||||
initNormally()
|
||||
expect(component.documentForm.disabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should support creating document type', () => {
|
||||
initNormally()
|
||||
let openModal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
|
||||
const modalSpy = jest.spyOn(modalService, 'open')
|
||||
component.createDocumentType('NewDocType2')
|
||||
expect(modalSpy).toHaveBeenCalled()
|
||||
openModal.componentInstance.succeeded.next({ id: 12, name: 'NewDocType12' })
|
||||
expect(component.documentForm.get('document_type').value).toEqual(12)
|
||||
})
|
||||
|
||||
it('should support creating correspondent', () => {
|
||||
initNormally()
|
||||
let openModal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
|
||||
const modalSpy = jest.spyOn(modalService, 'open')
|
||||
component.createCorrespondent('NewCorrrespondent12')
|
||||
expect(modalSpy).toHaveBeenCalled()
|
||||
openModal.componentInstance.succeeded.next({
|
||||
id: 12,
|
||||
name: 'NewCorrrespondent12',
|
||||
})
|
||||
expect(component.documentForm.get('correspondent').value).toEqual(12)
|
||||
})
|
||||
|
||||
it('should support creating storage path', () => {
|
||||
initNormally()
|
||||
let openModal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
|
||||
const modalSpy = jest.spyOn(modalService, 'open')
|
||||
component.createStoragePath('NewStoragePath12')
|
||||
expect(modalSpy).toHaveBeenCalled()
|
||||
openModal.componentInstance.succeeded.next({
|
||||
id: 12,
|
||||
name: 'NewStoragePath12',
|
||||
})
|
||||
expect(component.documentForm.get('storage_path').value).toEqual(12)
|
||||
})
|
||||
|
||||
it('should allow dischard changes', () => {
|
||||
initNormally()
|
||||
component.title = 'Foo Bar'
|
||||
fixture.detectChanges()
|
||||
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
|
||||
component.discard()
|
||||
fixture.detectChanges()
|
||||
expect(component.title).toEqual(doc.title)
|
||||
expect(openDocumentsService.hasDirty()).toBeFalsy()
|
||||
// this time with error, mostly for coverage
|
||||
component.title = 'Foo Bar'
|
||||
fixture.detectChanges()
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
jest
|
||||
.spyOn(documentService, 'get')
|
||||
.mockReturnValueOnce(throwError(() => new Error('unable to discard')))
|
||||
component.discard()
|
||||
fixture.detectChanges()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['404'])
|
||||
})
|
||||
|
||||
it('should 404 on invalid id', () => {
|
||||
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(null))
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
fixture.detectChanges()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['404'])
|
||||
})
|
||||
|
||||
it('should support save, close and show success toast', () => {
|
||||
initNormally()
|
||||
component.title = 'Foo Bar'
|
||||
const closeSpy = jest.spyOn(component, 'close')
|
||||
const updateSpy = jest.spyOn(documentService, 'update')
|
||||
const toastSpy = jest.spyOn(toastService, 'showInfo')
|
||||
updateSpy.mockImplementation((o) => of(doc))
|
||||
component.save(true)
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
|
||||
})
|
||||
|
||||
it('should support save without close and show success toast', () => {
|
||||
initNormally()
|
||||
component.title = 'Foo Bar'
|
||||
const closeSpy = jest.spyOn(component, 'close')
|
||||
const updateSpy = jest.spyOn(documentService, 'update')
|
||||
const toastSpy = jest.spyOn(toastService, 'showInfo')
|
||||
updateSpy.mockImplementation((o) => of(doc))
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
|
||||
})
|
||||
|
||||
it('should show toast error on save if error occurs', () => {
|
||||
currentUserHasObjectPermissions = true
|
||||
initNormally()
|
||||
component.title = 'Foo Bar'
|
||||
const closeSpy = jest.spyOn(component, 'close')
|
||||
const updateSpy = jest.spyOn(documentService, 'update')
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
updateSpy.mockImplementation(() =>
|
||||
throwError(() => new Error('failed to save'))
|
||||
)
|
||||
component.save()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
'Error saving document: failed to save'
|
||||
)
|
||||
})
|
||||
|
||||
it('should show error toast on save but close if user can no longer edit', () => {
|
||||
currentUserHasObjectPermissions = false
|
||||
initNormally()
|
||||
component.title = 'Foo Bar'
|
||||
const closeSpy = jest.spyOn(component, 'close')
|
||||
const updateSpy = jest.spyOn(documentService, 'update')
|
||||
const toastSpy = jest.spyOn(toastService, 'showInfo')
|
||||
updateSpy.mockImplementation(() =>
|
||||
throwError(() => new Error('failed to save'))
|
||||
)
|
||||
component.save(true)
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith('Document saved successfully.')
|
||||
})
|
||||
|
||||
it('should allow save and next', () => {
|
||||
initNormally()
|
||||
const nextDocId = 100
|
||||
component.title = 'Foo Bar'
|
||||
const updateSpy = jest.spyOn(documentService, 'update')
|
||||
updateSpy.mockReturnValue(of(doc))
|
||||
const nextSpy = jest.spyOn(documentListViewService, 'getNext')
|
||||
nextSpy.mockReturnValue(of(nextDocId))
|
||||
const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
|
||||
closeSpy.mockReturnValue(of(true))
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
|
||||
component.saveEditNext()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['documents', nextDocId])
|
||||
expect
|
||||
})
|
||||
|
||||
it('should show toast error on save & next if error occurs', () => {
|
||||
currentUserHasObjectPermissions = true
|
||||
initNormally()
|
||||
component.title = 'Foo Bar'
|
||||
const closeSpy = jest.spyOn(component, 'close')
|
||||
const updateSpy = jest.spyOn(documentService, 'update')
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
updateSpy.mockImplementation(() =>
|
||||
throwError(() => new Error('failed to save'))
|
||||
)
|
||||
component.saveEditNext()
|
||||
expect(updateSpy).toHaveBeenCalled()
|
||||
expect(closeSpy).not.toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
'Error saving document: failed to save'
|
||||
)
|
||||
})
|
||||
|
||||
it('should show save button and save & close or save & next', () => {
|
||||
const nextSpy = jest.spyOn(component, 'hasNext')
|
||||
nextSpy.mockReturnValueOnce(false)
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
fixture.debugElement
|
||||
.queryAll(By.css('button'))
|
||||
.find((b) => b.nativeElement.textContent === 'Save')
|
||||
).not.toBeUndefined()
|
||||
expect(
|
||||
fixture.debugElement
|
||||
.queryAll(By.css('button'))
|
||||
.find((b) => b.nativeElement.textContent === 'Save & close')
|
||||
).not.toBeUndefined()
|
||||
expect(
|
||||
fixture.debugElement
|
||||
.queryAll(By.css('button'))
|
||||
.find((b) => b.nativeElement.textContent === 'Save & next')
|
||||
).toBeUndefined()
|
||||
nextSpy.mockReturnValue(true)
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
fixture.debugElement
|
||||
.queryAll(By.css('button'))
|
||||
.find((b) => b.nativeElement.textContent === 'Save & close')
|
||||
).toBeUndefined()
|
||||
expect(
|
||||
fixture.debugElement
|
||||
.queryAll(By.css('button'))
|
||||
.find((b) => b.nativeElement.textContent === 'Save & next')
|
||||
).not.toBeUndefined()
|
||||
})
|
||||
|
||||
it('should allow close and navigate to documents by default', () => {
|
||||
initNormally()
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
component.close()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['documents'])
|
||||
})
|
||||
|
||||
it('should allow close and navigate to documents by default', () => {
|
||||
initNormally()
|
||||
jest
|
||||
.spyOn(documentListViewService, 'activeSavedViewId', 'get')
|
||||
.mockReturnValue(77)
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
component.close()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['view', 77])
|
||||
})
|
||||
|
||||
it('should not close if e.g. user-cancelled', () => {
|
||||
initNormally()
|
||||
jest.spyOn(openDocumentsService, 'closeDocument').mockReturnValue(of(false))
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
component.close()
|
||||
expect(navigateSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support delete, ask for confirmation', () => {
|
||||
initNormally()
|
||||
let openModal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
|
||||
const modalSpy = jest.spyOn(modalService, 'open')
|
||||
const deleteSpy = jest.spyOn(documentService, 'delete')
|
||||
deleteSpy.mockReturnValue(of(true))
|
||||
component.delete()
|
||||
expect(modalSpy).toHaveBeenCalled()
|
||||
const modalCloseSpy = jest.spyOn(openModal, 'close')
|
||||
openModal.componentInstance.confirmClicked.next()
|
||||
expect(deleteSpy).toHaveBeenCalled()
|
||||
expect(modalCloseSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should allow retry delete if error', () => {
|
||||
initNormally()
|
||||
let openModal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
|
||||
const modalSpy = jest.spyOn(modalService, 'open')
|
||||
const deleteSpy = jest.spyOn(documentService, 'delete')
|
||||
deleteSpy.mockReturnValueOnce(throwError(() => new Error('one time')))
|
||||
component.delete()
|
||||
expect(modalSpy).toHaveBeenCalled()
|
||||
const modalCloseSpy = jest.spyOn(openModal, 'close')
|
||||
openModal.componentInstance.confirmClicked.next()
|
||||
expect(deleteSpy).toHaveBeenCalled()
|
||||
expect(modalCloseSpy).not.toHaveBeenCalled()
|
||||
deleteSpy.mockReturnValueOnce(of(true))
|
||||
// retry
|
||||
openModal.componentInstance.confirmClicked.next()
|
||||
expect(deleteSpy).toHaveBeenCalled()
|
||||
expect(modalCloseSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support more like quick filter', () => {
|
||||
initNormally()
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.moreLike()
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
rule_type: FILTER_FULLTEXT_MORELIKE,
|
||||
value: doc.id.toString(),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should support redo ocr, confirm and close modal after started', () => {
|
||||
initNormally()
|
||||
const bulkEditSpy = jest.spyOn(documentService, 'bulkEdit')
|
||||
bulkEditSpy.mockReturnValue(of(true))
|
||||
let openModal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
|
||||
const modalSpy = jest.spyOn(modalService, 'open')
|
||||
const toastSpy = jest.spyOn(toastService, 'showInfo')
|
||||
component.redoOcr()
|
||||
const modalCloseSpy = jest.spyOn(openModal, 'close')
|
||||
openModal.componentInstance.confirmClicked.next()
|
||||
expect(bulkEditSpy).toHaveBeenCalledWith([doc.id], 'redo_ocr', {})
|
||||
expect(modalSpy).toHaveBeenCalled()
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
expect(modalCloseSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show error if redo ocr call fails', () => {
|
||||
initNormally()
|
||||
const bulkEditSpy = jest.spyOn(documentService, 'bulkEdit')
|
||||
let openModal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
component.redoOcr()
|
||||
const modalCloseSpy = jest.spyOn(openModal, 'close')
|
||||
bulkEditSpy.mockReturnValue(throwError(() => new Error('error occurred')))
|
||||
openModal.componentInstance.confirmClicked.next()
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
expect(modalCloseSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support next doc', () => {
|
||||
initNormally()
|
||||
const serviceSpy = jest.spyOn(documentListViewService, 'getNext')
|
||||
const routerSpy = jest.spyOn(router, 'navigate')
|
||||
serviceSpy.mockReturnValue(of(100))
|
||||
component.nextDoc()
|
||||
expect(serviceSpy).toHaveBeenCalled()
|
||||
expect(routerSpy).toHaveBeenCalledWith(['documents', 100])
|
||||
})
|
||||
|
||||
it('should support previous doc', () => {
|
||||
initNormally()
|
||||
const serviceSpy = jest.spyOn(documentListViewService, 'getPrevious')
|
||||
const routerSpy = jest.spyOn(router, 'navigate')
|
||||
serviceSpy.mockReturnValue(of(100))
|
||||
component.previousDoc()
|
||||
expect(serviceSpy).toHaveBeenCalled()
|
||||
expect(routerSpy).toHaveBeenCalledWith(['documents', 100])
|
||||
})
|
||||
|
||||
it('should support password-protected PDFs with a password field', () => {
|
||||
initNormally()
|
||||
component.onError({ name: 'PasswordException' }) // normally dispatched by pdf viewer
|
||||
expect(component.requiresPassword).toBeTruthy()
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
fixture.debugElement.query(By.css('input[type=password]'))
|
||||
).not.toBeUndefined()
|
||||
component.password = 'foo'
|
||||
component.pdfPreviewLoaded({ numPages: 1000 } as any)
|
||||
expect(component.requiresPassword).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should support Enter key in password field', () => {
|
||||
initNormally()
|
||||
component.onError({ name: 'PasswordException' }) // normally dispatched by pdf viewer
|
||||
fixture.detectChanges()
|
||||
expect(component.password).toBeUndefined()
|
||||
const pwField = fixture.debugElement.query(By.css('input[type=password]'))
|
||||
pwField.nativeElement.value = 'foobar'
|
||||
pwField.nativeElement.dispatchEvent(
|
||||
new KeyboardEvent('keyup', { key: 'Enter' })
|
||||
)
|
||||
expect(component.password).toEqual('foobar')
|
||||
})
|
||||
|
||||
it('should update n pages after pdf loaded', () => {
|
||||
initNormally()
|
||||
component.pdfPreviewLoaded({ numPages: 1000 } as any)
|
||||
expect(component.previewNumPages).toEqual(1000)
|
||||
})
|
||||
|
||||
it('should support updating notes dynamically', () => {
|
||||
const notes = [
|
||||
{
|
||||
id: 1,
|
||||
note: 'hello world',
|
||||
},
|
||||
]
|
||||
initNormally()
|
||||
const refreshSpy = jest.spyOn(openDocumentsService, 'refreshDocument')
|
||||
component.notesUpdated(notes) // called by notes component
|
||||
expect(component.document.notes).toEqual(notes)
|
||||
expect(refreshSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support quick filtering by correspondent', () => {
|
||||
initNormally()
|
||||
const object = {
|
||||
id: 22,
|
||||
name: 'Correspondent22',
|
||||
last_correspondence: new Date(),
|
||||
} as PaperlessCorrespondent
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.filterDocuments([object])
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
rule_type: FILTER_CORRESPONDENT,
|
||||
value: object.id.toString(),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should support quick filtering by doc type', () => {
|
||||
initNormally()
|
||||
const object = { id: 22, name: 'DocumentType22' } as PaperlessDocumentType
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.filterDocuments([object])
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
rule_type: FILTER_DOCUMENT_TYPE,
|
||||
value: object.id.toString(),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should support quick filtering by storage path', () => {
|
||||
initNormally()
|
||||
const object = {
|
||||
id: 22,
|
||||
name: 'StoragePath22',
|
||||
path: '/foo/bar/',
|
||||
} as PaperlessStoragePath
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.filterDocuments([object])
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
rule_type: FILTER_STORAGE_PATH,
|
||||
value: object.id.toString(),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should support quick filtering by all tags', () => {
|
||||
initNormally()
|
||||
const object1 = {
|
||||
id: 22,
|
||||
name: 'Tag22',
|
||||
is_inbox_tag: true,
|
||||
color: '#ff0000',
|
||||
text_color: '#000000',
|
||||
} as PaperlessTag
|
||||
const object2 = {
|
||||
id: 23,
|
||||
name: 'Tag22',
|
||||
is_inbox_tag: true,
|
||||
color: '#ff0000',
|
||||
text_color: '#000000',
|
||||
} as PaperlessTag
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.filterDocuments([object1, object2])
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
rule_type: FILTER_HAS_TAGS_ALL,
|
||||
value: object1.id.toString(),
|
||||
},
|
||||
{
|
||||
rule_type: FILTER_HAS_TAGS_ALL,
|
||||
value: object2.id.toString(),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should support quick filtering by date after - 1d and before +1d', () => {
|
||||
initNormally()
|
||||
const object = { year: 2023, month: 5, day: 14 } as NgbDateStruct
|
||||
const qfSpy = jest.spyOn(documentListViewService, 'quickFilter')
|
||||
component.filterDocuments([object])
|
||||
expect(qfSpy).toHaveBeenCalledWith([
|
||||
{
|
||||
rule_type: FILTER_CREATED_AFTER,
|
||||
value: '2023-05-13',
|
||||
},
|
||||
{
|
||||
rule_type: FILTER_CREATED_BEFORE,
|
||||
value: '2023-05-15',
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('should detect RTL languages and add css class to content textarea', () => {
|
||||
initNormally()
|
||||
component.metadata = { lang: 'he' }
|
||||
component.nav.select(2) // content
|
||||
fixture.detectChanges()
|
||||
expect(component.isRTL).toBeTruthy()
|
||||
expect(fixture.debugElement.queryAll(By.css('textarea.rtl'))).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should display built-in pdf viewer if not disabled', () => {
|
||||
initNormally()
|
||||
component.metadata = { has_archive_version: true }
|
||||
jest.spyOn(settingsService, 'get').mockReturnValue(false)
|
||||
expect(component.useNativePdfViewer).toBeFalsy()
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.query(By.css('pdf-viewer'))).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should display native pdf viewer if enabled', () => {
|
||||
initNormally()
|
||||
component.metadata = { has_archive_version: true }
|
||||
jest.spyOn(settingsService, 'get').mockReturnValue(true)
|
||||
expect(component.useNativePdfViewer).toBeTruthy()
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.query(By.css('object'))).not.toBeNull()
|
||||
})
|
||||
|
||||
it('should attempt to retrieve metadata', () => {
|
||||
const metadataSpy = jest.spyOn(documentService, 'getMetadata')
|
||||
metadataSpy.mockReturnValue(of({ has_archive_version: true }))
|
||||
initNormally()
|
||||
expect(metadataSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should show an error if failed metadata retrieval', () => {
|
||||
const error = new Error('metadata error')
|
||||
jest
|
||||
.spyOn(documentService, 'getMetadata')
|
||||
.mockReturnValue(throwError(() => error))
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
initNormally()
|
||||
expect(toastSpy).toHaveBeenCalledWith(
|
||||
'Error retrieving metadata',
|
||||
10000,
|
||||
error
|
||||
)
|
||||
})
|
||||
|
||||
function initNormally() {
|
||||
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
|
||||
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(null)
|
||||
jest
|
||||
.spyOn(openDocumentsService, 'openDocument')
|
||||
.mockReturnValueOnce(of(true))
|
||||
fixture.detectChanges()
|
||||
}
|
||||
})
|
@@ -40,9 +40,6 @@ import {
|
||||
FILTER_CORRESPONDENT,
|
||||
FILTER_CREATED_AFTER,
|
||||
FILTER_CREATED_BEFORE,
|
||||
FILTER_CREATED_DAY,
|
||||
FILTER_CREATED_MONTH,
|
||||
FILTER_CREATED_YEAR,
|
||||
FILTER_DOCUMENT_TYPE,
|
||||
FILTER_FULLTEXT_MORELIKE,
|
||||
FILTER_HAS_TAGS_ALL,
|
||||
@@ -62,8 +59,9 @@ import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { PaperlessDocumentNote } from 'src/app/data/paperless-document-note'
|
||||
import { HttpClient } from '@angular/common/http'
|
||||
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
|
||||
import { FilterRule } from 'src/app/data/filter-rule'
|
||||
import { EditDialogMode } from '../common/edit-dialog/edit-dialog.component'
|
||||
import { ObjectWithId } from 'src/app/data/object-with-id'
|
||||
import { FilterRule } from 'src/app/data/filter-rule'
|
||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||
|
||||
enum DocumentDetailNavIDs {
|
||||
@@ -438,7 +436,7 @@ export class DocumentDetailComponent
|
||||
var modal = this.modalService.open(DocumentTypeEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.dialogMode = 'create'
|
||||
modal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||
if (newName) modal.componentInstance.object = { name: newName }
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(
|
||||
@@ -459,7 +457,7 @@ export class DocumentDetailComponent
|
||||
var modal = this.modalService.open(CorrespondentEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.dialogMode = 'create'
|
||||
modal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||
if (newName) modal.componentInstance.object = { name: newName }
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(
|
||||
@@ -482,7 +480,7 @@ export class DocumentDetailComponent
|
||||
var modal = this.modalService.open(StoragePathEditDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.dialogMode = 'create'
|
||||
modal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||
if (newName) modal.componentInstance.object = { name: newName }
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(
|
||||
@@ -520,7 +518,7 @@ export class DocumentDetailComponent
|
||||
})
|
||||
}
|
||||
|
||||
save() {
|
||||
save(close: boolean = false) {
|
||||
this.networkActive = true
|
||||
this.documentsService
|
||||
.update(this.document)
|
||||
@@ -529,7 +527,7 @@ export class DocumentDetailComponent
|
||||
next: () => {
|
||||
this.store.next(this.documentForm.value)
|
||||
this.toastService.showInfo($localize`Document saved successfully.`)
|
||||
this.close()
|
||||
close && this.close()
|
||||
this.networkActive = false
|
||||
this.error = null
|
||||
},
|
||||
@@ -537,7 +535,7 @@ export class DocumentDetailComponent
|
||||
this.networkActive = false
|
||||
if (!this.userCanEdit) {
|
||||
this.toastService.showInfo($localize`Document saved successfully.`)
|
||||
this.close()
|
||||
close && this.close()
|
||||
} else {
|
||||
this.error = error.error
|
||||
this.toastService.showError(
|
||||
|
@@ -0,0 +1,51 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { MetadataCollapseComponent } from './metadata-collapse.component'
|
||||
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
|
||||
const metadata = [
|
||||
{
|
||||
namespace: 'http://ns.adobe.com/pdf/1.3/',
|
||||
prefix: 'pdf',
|
||||
key: 'Producer',
|
||||
value: 'pikepdf 2.2.0',
|
||||
},
|
||||
{
|
||||
namespace: 'http://ns.adobe.com/xap/1.0/',
|
||||
prefix: 'xmp',
|
||||
key: 'ModifyDate',
|
||||
value: '2020-12-21T08:42:26+00:00',
|
||||
},
|
||||
]
|
||||
|
||||
describe('MetadataCollapseComponent', () => {
|
||||
let component: MetadataCollapseComponent
|
||||
let fixture: ComponentFixture<MetadataCollapseComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [MetadataCollapseComponent],
|
||||
providers: [],
|
||||
imports: [NgbCollapseModule],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(MetadataCollapseComponent)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
it('should display metadata', () => {
|
||||
component.title = 'Foo'
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain('Foo')
|
||||
})
|
||||
|
||||
it('should display metadata', () => {
|
||||
component.metadata = metadata
|
||||
fixture.detectChanges()
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||
'pikepdf 2.2.0'
|
||||
)
|
||||
expect(fixture.debugElement.nativeElement.textContent).toContain(
|
||||
'ModifyDate'
|
||||
)
|
||||
})
|
||||
})
|
@@ -0,0 +1,869 @@
|
||||
import {
|
||||
HttpTestingController,
|
||||
HttpClientTestingModule,
|
||||
} from '@angular/common/http/testing'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import {
|
||||
NgbModal,
|
||||
NgbModule,
|
||||
NgbModalModule,
|
||||
NgbModalRef,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { of, throwError } from 'rxjs'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { FilterPipe } from 'src/app/pipes/filter.pipe'
|
||||
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
|
||||
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
|
||||
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
|
||||
import {
|
||||
SelectionData,
|
||||
DocumentService,
|
||||
} from 'src/app/services/rest/document.service'
|
||||
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
|
||||
import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { environment } from 'src/environments/environment'
|
||||
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
|
||||
import { FilterableDropdownComponent } from '../../common/filterable-dropdown/filterable-dropdown.component'
|
||||
import { ToggleableDropdownButtonComponent } from '../../common/filterable-dropdown/toggleable-dropdown-button/toggleable-dropdown-button.component'
|
||||
import { PermissionsDialogComponent } from '../../common/permissions-dialog/permissions-dialog.component'
|
||||
import { PermissionsFormComponent } from '../../common/input/permissions/permissions-form/permissions-form.component'
|
||||
import { BulkEditorComponent } from './bulk-editor.component'
|
||||
import { SelectComponent } from '../../common/input/select/select.component'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { PermissionsGroupComponent } from '../../common/input/permissions/permissions-group/permissions-group.component'
|
||||
import { PermissionsUserComponent } from '../../common/input/permissions/permissions-user/permissions-user.component'
|
||||
import { NgSelectModule } from '@ng-select/ng-select'
|
||||
import { GroupService } from 'src/app/services/rest/group.service'
|
||||
|
||||
const selectionData: SelectionData = {
|
||||
selected_tags: [
|
||||
{ id: 12, document_count: 3 },
|
||||
{ id: 22, document_count: 1 },
|
||||
{ id: 19, document_count: 0 },
|
||||
],
|
||||
selected_correspondents: [{ id: 33, document_count: 1 }],
|
||||
selected_document_types: [{ id: 44, document_count: 3 }],
|
||||
selected_storage_paths: [
|
||||
{ id: 66, document_count: 3 },
|
||||
{ id: 55, document_count: 0 },
|
||||
],
|
||||
}
|
||||
|
||||
describe('BulkEditorComponent', () => {
|
||||
let component: BulkEditorComponent
|
||||
let fixture: ComponentFixture<BulkEditorComponent>
|
||||
let permissionsService: PermissionsService
|
||||
let documentListViewService: DocumentListViewService
|
||||
let documentService: DocumentService
|
||||
let toastService: ToastService
|
||||
let modalService: NgbModal
|
||||
let httpTestingController: HttpTestingController
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
BulkEditorComponent,
|
||||
IfPermissionsDirective,
|
||||
FilterableDropdownComponent,
|
||||
ToggleableDropdownButtonComponent,
|
||||
FilterPipe,
|
||||
ConfirmDialogComponent,
|
||||
SafeHtmlPipe,
|
||||
PermissionsDialogComponent,
|
||||
PermissionsFormComponent,
|
||||
SelectComponent,
|
||||
PermissionsGroupComponent,
|
||||
PermissionsUserComponent,
|
||||
],
|
||||
providers: [
|
||||
PermissionsService,
|
||||
{
|
||||
provide: TagService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{ id: 12, name: 'tag12' },
|
||||
{ id: 22, name: 'tag22' },
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: CorrespondentService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [{ id: 33, name: 'correspondent33' }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: DocumentTypeService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [{ id: 44, name: 'doctype44' }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: StoragePathService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [
|
||||
{ id: 66, name: 'storagepath66' },
|
||||
{ id: 55, name: 'storagepath55' },
|
||||
],
|
||||
}),
|
||||
},
|
||||
},
|
||||
FilterPipe,
|
||||
SettingsService,
|
||||
{
|
||||
provide: UserService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [{ id: 1, username: 'user1' }],
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: GroupService,
|
||||
useValue: {
|
||||
listAll: () =>
|
||||
of({
|
||||
results: [],
|
||||
}),
|
||||
},
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
NgbModule,
|
||||
NgbModalModule,
|
||||
NgSelectModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
documentListViewService = TestBed.inject(DocumentListViewService)
|
||||
documentService = TestBed.inject(DocumentService)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
httpTestingController = TestBed.inject(HttpTestingController)
|
||||
|
||||
fixture = TestBed.createComponent(BulkEditorComponent)
|
||||
component = fixture.componentInstance
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
httpTestingController.verify()
|
||||
})
|
||||
|
||||
it('should apply selection data to tags menu', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
fixture.detectChanges()
|
||||
expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 5, 7]))
|
||||
jest
|
||||
.spyOn(documentService, 'getSelectionData')
|
||||
.mockReturnValue(of(selectionData))
|
||||
component.openTagsDropdown()
|
||||
expect(component.tagSelectionModel.selectionSize()).toEqual(1)
|
||||
})
|
||||
|
||||
it('should apply selection data to correspondents menu', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
component.correspondentSelectionModel.getSelectedItems()
|
||||
).toHaveLength(0)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 5, 7]))
|
||||
jest
|
||||
.spyOn(documentService, 'getSelectionData')
|
||||
.mockReturnValue(of(selectionData))
|
||||
component.openCorrespondentDropdown()
|
||||
expect(component.correspondentSelectionModel.items).toHaveLength(2)
|
||||
expect(component.correspondentSelectionModel.selectionSize()).toEqual(0)
|
||||
})
|
||||
|
||||
it('should apply selection data to doc types menu', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
component.documentTypeSelectionModel.getSelectedItems()
|
||||
).toHaveLength(0)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 5, 7]))
|
||||
jest
|
||||
.spyOn(documentService, 'getSelectionData')
|
||||
.mockReturnValue(of(selectionData))
|
||||
component.openDocumentTypeDropdown()
|
||||
expect(component.documentTypeSelectionModel.selectionSize()).toEqual(1)
|
||||
})
|
||||
|
||||
it('should apply selection data to storage path menu', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
component.storagePathsSelectionModel.getSelectedItems()
|
||||
).toHaveLength(0)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 5, 7]))
|
||||
jest
|
||||
.spyOn(documentService, 'getSelectionData')
|
||||
.mockReturnValue(of(selectionData))
|
||||
component.openStoragePathDropdown()
|
||||
expect(component.storagePathsSelectionModel.selectionSize()).toEqual(1)
|
||||
})
|
||||
|
||||
it('should execute modify tags bulk operation', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = false
|
||||
fixture.detectChanges()
|
||||
component.setTags({
|
||||
itemsToAdd: [{ id: 101 }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
method: 'modify_tags',
|
||||
parameters: { add_tags: [101], remove_tags: [] },
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should execute modify tags bulk operation with confirmation dialog if enabled', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.setTags({
|
||||
itemsToAdd: [{ id: 101 }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.confirm()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||
.flush(true)
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should set modal dialog text accordingly for tag edit confirmation', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.setTags({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [{ id: 101, name: 'Tag 101' }],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will remove the tag "Tag 101" from 2 selected document(s).'
|
||||
)
|
||||
modal.close()
|
||||
component.setTags({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [
|
||||
{ id: 101, name: 'Tag 101' },
|
||||
{ id: 102, name: 'Tag 102' },
|
||||
],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will remove the tags "Tag 101" and "Tag 102" from 2 selected document(s).'
|
||||
)
|
||||
modal.close()
|
||||
component.setTags({
|
||||
itemsToAdd: [
|
||||
{ id: 101, name: 'Tag 101' },
|
||||
{ id: 102, name: 'Tag 102' },
|
||||
],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will add the tags "Tag 101" and "Tag 102" to 2 selected document(s).'
|
||||
)
|
||||
modal.close()
|
||||
component.setTags({
|
||||
itemsToAdd: [
|
||||
{ id: 101, name: 'Tag 101' },
|
||||
{ id: 102, name: 'Tag 102' },
|
||||
],
|
||||
itemsToRemove: [{ id: 103, name: 'Tag 103' }],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will add the tags "Tag 101" and "Tag 102" and remove the tags "Tag 103" on 2 selected document(s).'
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute modify correspondent bulk operation', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = false
|
||||
fixture.detectChanges()
|
||||
component.setCorrespondents({
|
||||
itemsToAdd: [{ id: 101 }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
method: 'set_correspondent',
|
||||
parameters: { correspondent: 101 },
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should execute modify correspondent bulk operation with confirmation dialog if enabled', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.setCorrespondents({
|
||||
itemsToAdd: [{ id: 101 }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.confirm()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||
.flush(true)
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should set modal dialog text accordingly for correspondent edit confirmation', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.setCorrespondents({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [{ id: 101, name: 'Correspondent 101' }],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will remove the correspondent from 2 selected document(s).'
|
||||
)
|
||||
modal.close()
|
||||
component.setCorrespondents({
|
||||
itemsToAdd: [{ id: 101, name: 'Correspondent 101' }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will assign the correspondent "Correspondent 101" to 2 selected document(s).'
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute modify document type bulk operation', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = false
|
||||
fixture.detectChanges()
|
||||
component.setDocumentTypes({
|
||||
itemsToAdd: [{ id: 101 }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
method: 'set_document_type',
|
||||
parameters: { document_type: 101 },
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should execute modify document type bulk operation with confirmation dialog if enabled', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.setDocumentTypes({
|
||||
itemsToAdd: [{ id: 101 }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.confirm()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||
.flush(true)
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should set modal dialog text accordingly for document type edit confirmation', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.setDocumentTypes({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [{ id: 101, name: 'DocType 101' }],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will remove the document type from 2 selected document(s).'
|
||||
)
|
||||
modal.close()
|
||||
component.setDocumentTypes({
|
||||
itemsToAdd: [{ id: 101, name: 'DocType 101' }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will assign the document type "DocType 101" to 2 selected document(s).'
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute modify storage path bulk operation', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = false
|
||||
fixture.detectChanges()
|
||||
component.setStoragePaths({
|
||||
itemsToAdd: [{ id: 101 }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
method: 'set_storage_path',
|
||||
parameters: { storage_path: 101 },
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should execute modify storage path bulk operation with confirmation dialog if enabled', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.setStoragePaths({
|
||||
itemsToAdd: [{ id: 101 }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.confirm()
|
||||
httpTestingController
|
||||
.expectOne(`${environment.apiBaseUrl}documents/bulk_edit/`)
|
||||
.flush(true)
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should set modal dialog text accordingly for storage path edit confirmation', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.setStoragePaths({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [{ id: 101, name: 'StoragePath 101' }],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will remove the storage path from 2 selected document(s).'
|
||||
)
|
||||
modal.close()
|
||||
component.setStoragePaths({
|
||||
itemsToAdd: [{ id: 101, name: 'StoragePath 101' }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
expect(modal.componentInstance.message).toEqual(
|
||||
'This operation will assign the storage path "StoragePath 101" to 2 selected document(s).'
|
||||
)
|
||||
})
|
||||
|
||||
it('should only execute bulk operations when changes are detected', () => {
|
||||
component.setTags({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
component.setCorrespondents({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
component.setDocumentTypes({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
component.setStoragePaths({
|
||||
itemsToAdd: [],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
httpTestingController.expectNone(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
})
|
||||
|
||||
it('should support bulk delete with confirmation', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.applyDelete()
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.confirm()
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
method: 'delete',
|
||||
parameters: {},
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should not be accessible with insufficient global permissions', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
|
||||
fixture.detectChanges()
|
||||
const dropdown = fixture.debugElement.query(
|
||||
By.directive(FilterableDropdownComponent)
|
||||
)
|
||||
expect(dropdown).toBeNull()
|
||||
})
|
||||
|
||||
it('should disable with insufficient object permissions', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(false)
|
||||
fixture.detectChanges()
|
||||
const button = fixture.debugElement
|
||||
.query(By.directive(FilterableDropdownComponent))
|
||||
.query(By.css('button'))
|
||||
expect(button.nativeElement.disabled).toBeTruthy()
|
||||
})
|
||||
|
||||
it('should show a warning toast on bulk edit error', () => {
|
||||
jest
|
||||
.spyOn(documentService, 'bulkEdit')
|
||||
.mockReturnValue(
|
||||
throwError(() => new Error('error executing bulk operation'))
|
||||
)
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = false
|
||||
fixture.detectChanges()
|
||||
const toastSpy = jest.spyOn(toastService, 'showError')
|
||||
component.setTags({
|
||||
itemsToAdd: [{ id: 0 }],
|
||||
itemsToRemove: [],
|
||||
})
|
||||
expect(toastSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should support redo ocr', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.redoOcrSelected()
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.confirm()
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
method: 'redo_ocr',
|
||||
parameters: {},
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
|
||||
it('should support bulk download with archive, originals or both and file formatting', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.downloadForm.get('downloadFileTypeArchive').patchValue(true)
|
||||
fixture.detectChanges()
|
||||
let downloadSpy = jest.spyOn(documentService, 'bulkDownload')
|
||||
//archive
|
||||
component.downloadSelected()
|
||||
expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'archive', false)
|
||||
//originals
|
||||
component.downloadForm.get('downloadFileTypeArchive').patchValue(false)
|
||||
component.downloadForm.get('downloadFileTypeOriginals').patchValue(true)
|
||||
component.downloadSelected()
|
||||
expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'originals', false)
|
||||
//both
|
||||
component.downloadForm.get('downloadFileTypeArchive').patchValue(true)
|
||||
component.downloadSelected()
|
||||
expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'both', false)
|
||||
//formatting
|
||||
component.downloadForm.get('downloadUseFormatting').patchValue(true)
|
||||
component.downloadSelected()
|
||||
expect(downloadSpy).toHaveBeenCalledWith([3, 4], 'both', true)
|
||||
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/bulk_download/`
|
||||
)
|
||||
})
|
||||
|
||||
it('should support bulk permissions update', () => {
|
||||
let modal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((m) => (modal = m[0]))
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(documentListViewService, 'documents', 'get')
|
||||
.mockReturnValue([{ id: 3 }, { id: 4 }])
|
||||
jest
|
||||
.spyOn(documentListViewService, 'selected', 'get')
|
||||
.mockReturnValue(new Set([3, 4]))
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
.mockReturnValue(true)
|
||||
component.showConfirmationDialogs = true
|
||||
fixture.detectChanges()
|
||||
component.setPermissions()
|
||||
expect(modal).not.toBeUndefined()
|
||||
modal.componentInstance.confirmClicked.next()
|
||||
let req = httpTestingController.expectOne(
|
||||
`${environment.apiBaseUrl}documents/bulk_edit/`
|
||||
)
|
||||
req.flush(true)
|
||||
expect(req.request.body).toEqual({
|
||||
documents: [3, 4],
|
||||
method: 'set_permissions',
|
||||
parameters: undefined,
|
||||
})
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
|
||||
) // list reload
|
||||
httpTestingController.match(
|
||||
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
|
||||
) // listAllFilteredIds
|
||||
})
|
||||
})
|
@@ -0,0 +1,129 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import {
|
||||
NgbPopoverModule,
|
||||
NgbTooltipModule,
|
||||
NgbProgressbarModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
||||
import { DocumentCardLargeComponent } from './document-card-large.component'
|
||||
|
||||
const doc = {
|
||||
id: 10,
|
||||
title: 'Document 10',
|
||||
tags: [3, 4, 5],
|
||||
correspondent: 8,
|
||||
document_type: 10,
|
||||
storage_path: null,
|
||||
notes: [
|
||||
{
|
||||
id: 11,
|
||||
note: 'This is some note content bananas',
|
||||
},
|
||||
],
|
||||
content:
|
||||
'Cupcake ipsum dolor sit amet ice cream. Donut shortbread cheesecake caramels tiramisu pastry caramels chocolate bar. Tart tootsie roll muffin icing cotton candy topping sweet roll. Pie lollipop dragée sesame snaps donut tart pudding. Oat cake apple pie danish danish candy canes. Shortbread candy canes sesame snaps muffin tiramisu marshmallow chocolate bar halvah. Cake lemon drops candy apple pie carrot cake bonbon halvah pastry gummi bears. Sweet roll candy ice cream sesame snaps marzipan cookie ice cream. Cake cheesecake apple pie muffin candy toffee lollipop. Carrot cake oat cake cookie biscuit cupcake cake marshmallow. Sweet roll jujubes carrot cake cheesecake cake candy canes sweet roll gingerbread jelly beans. Apple pie sugar plum oat cake halvah cake. Pie oat cake chocolate cake cookie gingerbread marzipan. Lemon drops cheesecake lollipop danish marzipan candy.',
|
||||
}
|
||||
|
||||
describe('DocumentCardLargeComponent', () => {
|
||||
let component: DocumentCardLargeComponent
|
||||
let fixture: ComponentFixture<DocumentCardLargeComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DocumentCardLargeComponent,
|
||||
DocumentTitlePipe,
|
||||
CustomDatePipe,
|
||||
IfPermissionsDirective,
|
||||
SafeUrlPipe,
|
||||
],
|
||||
providers: [DatePipe],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule,
|
||||
NgbPopoverModule,
|
||||
NgbTooltipModule,
|
||||
NgbProgressbarModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(DocumentCardLargeComponent)
|
||||
component = fixture.componentInstance
|
||||
component.document = doc
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should display a document', () => {
|
||||
expect(fixture.nativeElement.textContent).toContain('Document 10')
|
||||
expect(fixture.nativeElement.textContent).toContain('Cupcake ipsum')
|
||||
})
|
||||
|
||||
it('should show preview on mouseover after delay to preload content', fakeAsync(() => {
|
||||
component.mouseEnterPreview()
|
||||
expect(component.popover.isOpen()).toBeTruthy()
|
||||
expect(component.popoverHidden).toBeTruthy()
|
||||
tick(600)
|
||||
expect(component.popoverHidden).toBeFalsy()
|
||||
component.mouseLeaveCard()
|
||||
|
||||
component.mouseEnterPreview()
|
||||
tick(100)
|
||||
component.mouseLeavePreview()
|
||||
tick(600)
|
||||
expect(component.popover.isOpen()).toBeFalsy()
|
||||
}))
|
||||
|
||||
it('should trim content', () => {
|
||||
expect(component.contentTrimmed).toHaveLength(503) // includes ...
|
||||
})
|
||||
|
||||
it('should display search hits with colored score', () => {
|
||||
// high
|
||||
component.document.__search_hit__ = {
|
||||
score: 0.9,
|
||||
rank: 1,
|
||||
highlights: 'cheesecake',
|
||||
}
|
||||
fixture.detectChanges()
|
||||
let search_hit = fixture.debugElement.query(By.css('.search-score'))
|
||||
expect(search_hit).not.toBeUndefined()
|
||||
expect(component.searchScoreClass).toEqual('success')
|
||||
|
||||
// medium
|
||||
component.document.__search_hit__.score = 0.6
|
||||
fixture.detectChanges()
|
||||
search_hit = fixture.debugElement.query(By.css('.search-score'))
|
||||
expect(search_hit).not.toBeUndefined()
|
||||
expect(component.searchScoreClass).toEqual('warning')
|
||||
|
||||
// low
|
||||
component.document.__search_hit__.score = 0.1
|
||||
fixture.detectChanges()
|
||||
search_hit = fixture.debugElement.query(By.css('.search-score'))
|
||||
expect(search_hit).not.toBeUndefined()
|
||||
expect(component.searchScoreClass).toEqual('danger')
|
||||
})
|
||||
|
||||
it('should display note highlights', () => {
|
||||
component.document.__search_hit__ = {
|
||||
score: 0.9,
|
||||
rank: 1,
|
||||
note_highlights: '<span>bananas</span>',
|
||||
}
|
||||
fixture.detectChanges()
|
||||
expect(fixture.nativeElement.textContent).toContain('bananas')
|
||||
expect(component.searchNoteHighlights).toContain('<span>bananas</span>')
|
||||
})
|
||||
})
|
@@ -133,7 +133,7 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions {
|
||||
|
||||
get contentTrimmed() {
|
||||
return (
|
||||
this.document.content.substr(0, 500) +
|
||||
this.document.content.substring(0, 500) +
|
||||
(this.document.content.length > 500 ? '...' : '')
|
||||
)
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@
|
||||
<div class="card-body bg-light p-2">
|
||||
<p class="card-text">
|
||||
<ng-container *ngIf="document.correspondent">
|
||||
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name}}</a>:
|
||||
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{(document.correspondent$ | async)?.name ?? privateName}}</a>:
|
||||
</ng-container>
|
||||
{{document.title | documentTitle}}
|
||||
</p>
|
||||
@@ -41,18 +41,18 @@
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#file-earmark"/>
|
||||
</svg>
|
||||
<small>{{(document.document_type$ | async)?.name}}</small>
|
||||
<small>{{(document.document_type$ | async)?.name ?? privateName}}</small>
|
||||
</button>
|
||||
<button *ngIf="document.storage_path" type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
|
||||
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
|
||||
<svg class="metadata-icon me-2 text-muted" fill="currentColor">
|
||||
<use xlink:href="assets/bootstrap-icons.svg#folder"/>
|
||||
</svg>
|
||||
<small>{{(document.storage_path$ | async)?.name}}</small>
|
||||
<small>{{(document.storage_path$ | async)?.name ?? privateName}}</small>
|
||||
</button>
|
||||
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
|
||||
<ng-template #dateTooltip>
|
||||
<div class="d-flex flex-column">
|
||||
<div class="d-flex flex-column text-light">
|
||||
<span i18n>Created: {{ document.created | customDate }}</span>
|
||||
<span i18n>Added: {{ document.added | customDate }}</span>
|
||||
<span i18n>Modified: {{ document.modified | customDate }}</span>
|
||||
|
@@ -0,0 +1,120 @@
|
||||
import { DatePipe } from '@angular/common'
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing'
|
||||
import {
|
||||
ComponentFixture,
|
||||
TestBed,
|
||||
fakeAsync,
|
||||
tick,
|
||||
} from '@angular/core/testing'
|
||||
import { RouterTestingModule } from '@angular/router/testing'
|
||||
import {
|
||||
NgbPopoverModule,
|
||||
NgbTooltipModule,
|
||||
NgbProgressbarModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
|
||||
import { DocumentTitlePipe } from 'src/app/pipes/document-title.pipe'
|
||||
import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
|
||||
import { DocumentCardSmallComponent } from './document-card-small.component'
|
||||
import { of } from 'rxjs'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { TagComponent } from '../../common/tag/tag.component'
|
||||
import { PaperlessTag } from 'src/app/data/paperless-tag'
|
||||
|
||||
const doc = {
|
||||
id: 10,
|
||||
title: 'Document 10',
|
||||
tags: [1, 2, 3, 4, 5, 6, 7, 8],
|
||||
correspondent: 8,
|
||||
document_type: 10,
|
||||
storage_path: null,
|
||||
notes: [
|
||||
{
|
||||
id: 11,
|
||||
note: 'This is some note content bananas',
|
||||
},
|
||||
],
|
||||
tags$: of([
|
||||
{ id: 1, name: 'Tag1' },
|
||||
{ id: 2, name: 'Tag2' },
|
||||
{ id: 3, name: 'Tag3' },
|
||||
{ id: 4, name: 'Tag4' },
|
||||
{ id: 5, name: 'Tag5' },
|
||||
{ id: 6, name: 'Tag6' },
|
||||
{ id: 7, name: 'Tag7' },
|
||||
{ id: 8, name: 'Tag8' },
|
||||
]),
|
||||
content:
|
||||
'Cupcake ipsum dolor sit amet ice cream. Donut shortbread cheesecake caramels tiramisu pastry caramels chocolate bar. Tart tootsie roll muffin icing cotton candy topping sweet roll. Pie lollipop dragée sesame snaps donut tart pudding. Oat cake apple pie danish danish candy canes. Shortbread candy canes sesame snaps muffin tiramisu marshmallow chocolate bar halvah. Cake lemon drops candy apple pie carrot cake bonbon halvah pastry gummi bears. Sweet roll candy ice cream sesame snaps marzipan cookie ice cream. Cake cheesecake apple pie muffin candy toffee lollipop. Carrot cake oat cake cookie biscuit cupcake cake marshmallow. Sweet roll jujubes carrot cake cheesecake cake candy canes sweet roll gingerbread jelly beans. Apple pie sugar plum oat cake halvah cake. Pie oat cake chocolate cake cookie gingerbread marzipan. Lemon drops cheesecake lollipop danish marzipan candy.',
|
||||
}
|
||||
|
||||
describe('DocumentCardSmallComponent', () => {
|
||||
let component: DocumentCardSmallComponent
|
||||
let fixture: ComponentFixture<DocumentCardSmallComponent>
|
||||
|
||||
beforeEach(async () => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [
|
||||
DocumentCardSmallComponent,
|
||||
DocumentTitlePipe,
|
||||
CustomDatePipe,
|
||||
IfPermissionsDirective,
|
||||
SafeUrlPipe,
|
||||
TagComponent,
|
||||
],
|
||||
providers: [DatePipe],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
RouterTestingModule,
|
||||
NgbPopoverModule,
|
||||
NgbTooltipModule,
|
||||
NgbProgressbarModule,
|
||||
],
|
||||
}).compileComponents()
|
||||
|
||||
fixture = TestBed.createComponent(DocumentCardSmallComponent)
|
||||
component = fixture.componentInstance
|
||||
component.document = Object.assign({}, doc)
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
it('should display a document, limit tags to 5', () => {
|
||||
expect(fixture.nativeElement.textContent).toContain('Document 10')
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.directive(TagComponent))
|
||||
).toHaveLength(5)
|
||||
component.document.tags = [1, 2]
|
||||
component.document.tags$ = of([
|
||||
{ id: 1 } as PaperlessTag,
|
||||
{ id: 2 } as PaperlessTag,
|
||||
])
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.directive(TagComponent))
|
||||
).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('should increase limit tags to 6 if no notes', () => {
|
||||
component.document.notes = []
|
||||
fixture.detectChanges()
|
||||
expect(
|
||||
fixture.debugElement.queryAll(By.directive(TagComponent))
|
||||
).toHaveLength(6)
|
||||
})
|
||||
|
||||
it('should show preview on mouseover after delay to preload content', fakeAsync(() => {
|
||||
component.mouseEnterPreview()
|
||||
expect(component.popover.isOpen()).toBeTruthy()
|
||||
expect(component.popoverHidden).toBeTruthy()
|
||||
tick(600)
|
||||
expect(component.popoverHidden).toBeFalsy()
|
||||
component.mouseLeaveCard()
|
||||
|
||||
component.mouseEnterPreview()
|
||||
tick(100)
|
||||
component.mouseLeavePreview()
|
||||
tick(600)
|
||||
expect(component.popover.isOpen()).toBeFalsy()
|
||||
}))
|
||||
})
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user