Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85e192f935 | ||
|
|
4fcd4961bb | ||
|
|
5d9401ac4a | ||
|
|
6935defe7c | ||
|
|
440049978b | ||
|
|
06e9c1c02b | ||
|
|
40d09ef309 |
@@ -185,22 +185,16 @@ jobs:
|
||||
flags: frontend-node-${{ matrix.node-version }}
|
||||
directory: src-ui/coverage/
|
||||
e2e-tests:
|
||||
name: "E2E Tests (${{ matrix.shard-index }}/${{ matrix.shard-count }})"
|
||||
name: E2E Tests
|
||||
needs: [changes, install-dependencies]
|
||||
if: needs.changes.outputs.frontend_changed == 'true'
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
contents: read
|
||||
container: mcr.microsoft.com/playwright:v1.62.0-noble
|
||||
container: mcr.microsoft.com/playwright:v1.62.1-noble
|
||||
env:
|
||||
PLAYWRIGHT_BROWSERS_PATH: /ms-playwright
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [24.x]
|
||||
shard-index: [1, 2]
|
||||
shard-count: [2]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
@@ -216,6 +210,17 @@ jobs:
|
||||
node-version: 24.x
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: 'src-ui/pnpm-lock.yaml'
|
||||
- name: Set up Python
|
||||
id: setup-python
|
||||
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
|
||||
with:
|
||||
python-version: '3.12'
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0
|
||||
with:
|
||||
version: '0.12.x'
|
||||
enable-cache: false
|
||||
python-version: ${{ steps.setup-python.outputs.python-version }}
|
||||
- name: Cache frontend dependencies
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
@@ -225,8 +230,17 @@ jobs:
|
||||
key: ${{ runner.os }}-frontend-${{ hashFiles('src-ui/pnpm-lock.yaml') }}
|
||||
- name: Install dependencies
|
||||
run: cd src-ui && pnpm install --frozen-lockfile
|
||||
- name: Install backend system dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install --yes --quiet --no-install-recommends libmagic1
|
||||
- name: Install backend dependencies
|
||||
env:
|
||||
PYTHON_VERSION: ${{ steps.setup-python.outputs.python-version }}
|
||||
# The frozen repository lockfile and its build hooks are trusted.
|
||||
run: uv sync --python "${PYTHON_VERSION}" --no-dev --frozen # NOSONAR
|
||||
- name: Run Playwright E2E tests
|
||||
run: cd src-ui && pnpm exec playwright test --shard ${{ matrix.shard-index }}/${{ matrix.shard-count }}
|
||||
run: cd src-ui && pnpm exec playwright test
|
||||
frontend-build:
|
||||
name: Frontend Build
|
||||
needs: [changes, unit-tests, e2e-tests]
|
||||
|
||||
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 501 KiB After Width: | Height: | Size: 487 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 558 KiB |
|
Before Width: | Height: | Size: 644 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 667 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1003 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.1 MiB After Width: | Height: | Size: 1.5 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 925 KiB After Width: | Height: | Size: 972 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 558 KiB |
|
Before Width: | Height: | Size: 726 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 432 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 280 KiB After Width: | Height: | Size: 322 KiB |
|
Before Width: | Height: | Size: 246 KiB After Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 559 KiB After Width: | Height: | Size: 516 KiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 230 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 333 KiB |
|
Before Width: | Height: | Size: 792 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 291 KiB |
@@ -1,5 +1,41 @@
|
||||
# Changelog
|
||||
|
||||
## paperless-ngx 3.1.1
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Fix: 3.1.0 llm suggestions remove existing metadata from prompt, dont drop name suggestions [@shamoon](https://github.com/shamoon) ([#13866](https://github.com/paperless-ngx/paperless-ngx/pull/13866))
|
||||
- Fix: set global search earlier to avoid awaiting debounce [@shamoon](https://github.com/shamoon) ([#13865](https://github.com/paperless-ngx/paperless-ngx/pull/13865))
|
||||
- Fix: responsive sidebar, centralize and make sizes saner [@shamoon](https://github.com/shamoon) ([#13863](https://github.com/paperless-ngx/paperless-ngx/pull/13863))
|
||||
- Tweak/fix: show existing count for ai suggestions [@shamoon](https://github.com/shamoon) ([#13861](https://github.com/paperless-ngx/paperless-ngx/pull/13861))
|
||||
- Fix: 3.1.0 llm suggestions simplify schema, fix docstrings [@shamoon](https://github.com/shamoon) ([#13850](https://github.com/paperless-ngx/paperless-ngx/pull/13850))
|
||||
- Fix: ensure ui reset of suggestionsLoading when changing docs [@shamoon](https://github.com/shamoon) ([#13840](https://github.com/paperless-ngx/paperless-ngx/pull/13840))
|
||||
- Fix: always pass a non-empty api key for OpenAI-like servers [@shamoon](https://github.com/shamoon) ([#13838](https://github.com/paperless-ngx/paperless-ngx/pull/13838))
|
||||
- Fix: hide slim sidebar scrollbar in browsers with stupid scrollbars [@shamoon](https://github.com/shamoon) ([#13837](https://github.com/paperless-ngx/paperless-ngx/pull/13837))
|
||||
- Fix: correct sharelink bundle + document link permissions display bugs [@shamoon](https://github.com/shamoon) ([#13827](https://github.com/paperless-ngx/paperless-ngx/pull/13827))
|
||||
- Fix: immediately re-add doc to index after trash restore [@shamoon](https://github.com/shamoon) ([#13818](https://github.com/paperless-ngx/paperless-ngx/pull/13818))
|
||||
- Fix: navbar brand anchor size + Safari position jitter [@shamoon](https://github.com/shamoon) ([#13810](https://github.com/paperless-ngx/paperless-ngx/pull/13810))
|
||||
|
||||
### All App Changes
|
||||
|
||||
<details>
|
||||
<summary>12 changes</summary>
|
||||
|
||||
- Fix: 3.1.0 llm suggestions remove existing metadata from prompt, dont drop name suggestions [@shamoon](https://github.com/shamoon) ([#13866](https://github.com/paperless-ngx/paperless-ngx/pull/13866))
|
||||
- Fix: set global search earlier to avoid awaiting debounce [@shamoon](https://github.com/shamoon) ([#13865](https://github.com/paperless-ngx/paperless-ngx/pull/13865))
|
||||
- Fix: responsive sidebar, centralize and make sizes saner [@shamoon](https://github.com/shamoon) ([#13863](https://github.com/paperless-ngx/paperless-ngx/pull/13863))
|
||||
- Tweak/fix: show existing count for ai suggestions [@shamoon](https://github.com/shamoon) ([#13861](https://github.com/paperless-ngx/paperless-ngx/pull/13861))
|
||||
- Fix: 3.1.0 llm suggestions simplify schema, fix docstrings [@shamoon](https://github.com/shamoon) ([#13850](https://github.com/paperless-ngx/paperless-ngx/pull/13850))
|
||||
- Fixhancement: make imap port required, better error display [@shamoon](https://github.com/shamoon) ([#13845](https://github.com/paperless-ngx/paperless-ngx/pull/13845))
|
||||
- Fix: ensure ui reset of suggestionsLoading when changing docs [@shamoon](https://github.com/shamoon) ([#13840](https://github.com/paperless-ngx/paperless-ngx/pull/13840))
|
||||
- Fix: always pass a non-empty api key for OpenAI-like servers [@shamoon](https://github.com/shamoon) ([#13838](https://github.com/paperless-ngx/paperless-ngx/pull/13838))
|
||||
- Fix: hide slim sidebar scrollbar in browsers with stupid scrollbars [@shamoon](https://github.com/shamoon) ([#13837](https://github.com/paperless-ngx/paperless-ngx/pull/13837))
|
||||
- Fix: correct sharelink bundle + document link permissions display bugs [@shamoon](https://github.com/shamoon) ([#13827](https://github.com/paperless-ngx/paperless-ngx/pull/13827))
|
||||
- Fix: immediately re-add doc to index after trash restore [@shamoon](https://github.com/shamoon) ([#13818](https://github.com/paperless-ngx/paperless-ngx/pull/13818))
|
||||
- Fix: navbar brand anchor size + Safari position jitter [@shamoon](https://github.com/shamoon) ([#13810](https://github.com/paperless-ngx/paperless-ngx/pull/13810))
|
||||
|
||||
</details>
|
||||
|
||||
## paperless-ngx 3.1.0
|
||||
|
||||
### Features / Enhancements
|
||||
|
||||
@@ -224,13 +224,19 @@ respectively, can be run non-interactively with:
|
||||
|
||||
```bash
|
||||
pnpm ng test
|
||||
pnpm playwright test
|
||||
pnpm e2e
|
||||
```
|
||||
|
||||
The Playwright suite starts both the Angular development server and a disposable
|
||||
Paperless instance on port 8001. The instance uses SQLite, temporary data and
|
||||
media directories, and deterministic sample documents; it is removed when the
|
||||
test run finishes. This requires the back-end Python dependencies from the
|
||||
regular development setup to be installed with `uv sync`.
|
||||
|
||||
Playwright also includes a UI which can be run with:
|
||||
|
||||
```bash
|
||||
pnpm playwright test --ui
|
||||
pnpm e2e:ui
|
||||
```
|
||||
|
||||
### Building the frontend
|
||||
|
||||
@@ -139,6 +139,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"e2e": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.e2e.ts"
|
||||
}
|
||||
],
|
||||
"localize": [
|
||||
"en-US"
|
||||
]
|
||||
},
|
||||
"en-US": {
|
||||
"localize": [
|
||||
"en-US"
|
||||
@@ -155,6 +166,9 @@
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "paperless-ui:build:production"
|
||||
},
|
||||
"e2e": {
|
||||
"buildTarget": "paperless-ui:build:e2e"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,194 +0,0 @@
|
||||
{
|
||||
"log": {
|
||||
"version": "1.2",
|
||||
"creator": {
|
||||
"name": "Playwright",
|
||||
"version": "1.33.0"
|
||||
},
|
||||
"browser": {
|
||||
"name": "chromium",
|
||||
"version": "113.0.5672.53"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"startedDateTime": "2023-05-14T07:18:59.856Z",
|
||||
"time": 6.025,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/ui_settings/",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Accept", "value": "application/json; version=3" },
|
||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
||||
{ "name": "Accept-Language", "value": "en-US" },
|
||||
{ "name": "Connection", "value": "keep-alive" },
|
||||
{ "name": "Host", "value": "localhost:8000" },
|
||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": -1,
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
||||
{ "name": "Content-Encoding", "value": "br" },
|
||||
{ "name": "Content-Language", "value": "en-us" },
|
||||
{ "name": "Content-Length", "value": "953" },
|
||||
{ "name": "Content-Type", "value": "application/json" },
|
||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
||||
{ "name": "X-Api-Version", "value": "3" },
|
||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
||||
{ "name": "X-Version", "value": "1.14.4" }
|
||||
],
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":true,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[\"change_savedview\",\"change_schedule\",\"change_failure\",\"delete_token\",\"add_mailrule\",\"view_failure\",\"view_groupresult\",\"add_note\",\"change_taskresult\",\"view_tag\",\"view_user\",\"add_tag\",\"change_processedmail\",\"change_session\",\"view_taskattributes\",\"delete_groupresult\",\"delete_correspondent\",\"delete_schedule\",\"delete_contenttype\",\"view_chordcounter\",\"view_success\",\"delete_documenttype\",\"add_tokenproxy\",\"delete_paperlesstask\",\"add_log\",\"view_mailaccount\",\"add_uisettings\",\"view_savedview\",\"view_uisettings\",\"delete_storagepath\",\"delete_frontendsettings\",\"change_paperlesstask\",\"view_taskresult\",\"delete_processedmail\",\"view_processedmail\",\"view_session\",\"delete_chordcounter\",\"view_note\",\"delete_session\",\"view_document\",\"change_mailaccount\",\"delete_taskattributes\",\"add_groupobjectpermission\",\"view_mailrule\",\"change_savedviewfilterrule\",\"change_log\",\"change_comment\",\"add_mailaccount\",\"add_frontendsettings\",\"add_userobjectpermission\",\"delete_note\",\"view_token\",\"add_failure\",\"delete_user\",\"add_success\",\"view_ormq\",\"view_tokenproxy\",\"delete_uisettings\",\"change_groupobjectpermission\",\"add_logentry\",\"add_ormq\",\"view_frontendsettings\",\"view_schedule\",\"change_taskattributes\",\"view_documenttype\",\"view_logentry\",\"change_correspondent\",\"add_groupresult\",\"delete_groupobjectpermission\",\"change_mailrule\",\"change_permission\",\"delete_log\",\"view_userobjectpermission\",\"view_correspondent\",\"delete_document\",\"change_uisettings\",\"change_storagepath\",\"change_document\",\"delete_tokenproxy\",\"change_note\",\"delete_permission\",\"change_contenttype\",\"add_token\",\"change_success\",\"delete_logentry\",\"view_savedviewfilterrule\",\"delete_task\",\"add_savedview\",\"add_paperlesstask\",\"add_task\",\"change_documenttype\",\"add_documenttype\",\"change_token\",\"view_task\",\"view_permission\",\"change_task\",\"delete_userobjectpermission\",\"change_group\",\"add_group\",\"change_tag\",\"change_chordcounter\",\"add_storagepath\",\"delete_group\",\"add_taskattributes\",\"delete_mailaccount\",\"delete_tag\",\"add_schedule\",\"delete_failure\",\"delete_mailrule\",\"add_savedviewfilterrule\",\"change_ormq\",\"change_logentry\",\"add_taskresult\",\"view_group\",\"delete_comment\",\"add_contenttype\",\"add_document\",\"change_tokenproxy\",\"delete_success\",\"add_comment\",\"delete_ormq\",\"add_processedmail\",\"view_paperlesstask\",\"delete_savedview\",\"change_user\",\"add_session\",\"view_groupobjectpermission\",\"add_user\",\"add_correspondent\",\"delete_taskresult\",\"view_contenttype\",\"view_storagepath\",\"add_permission\",\"change_userobjectpermission\",\"delete_savedviewfilterrule\",\"change_groupresult\",\"add_chordcounter\",\"view_log\",\"view_comment\",\"change_frontendsettings\"]}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
"redirectURL": ""
|
||||
},
|
||||
"cache": {},
|
||||
"timings": { "send": -1, "wait": -1, "receive": 6.025 }
|
||||
},
|
||||
{
|
||||
"startedDateTime": "2023-05-14T07:18:59.990Z",
|
||||
"time": 1.082,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/saved_views/?page=1&page_size=100000",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Accept", "value": "application/json; version=3" },
|
||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
||||
{ "name": "Accept-Language", "value": "en-US" },
|
||||
{ "name": "Connection", "value": "keep-alive" },
|
||||
{ "name": "Host", "value": "localhost:8000" },
|
||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
||||
],
|
||||
"queryString": [
|
||||
{
|
||||
"name": "page",
|
||||
"value": "1"
|
||||
},
|
||||
{
|
||||
"name": "page_size",
|
||||
"value": "100000"
|
||||
}
|
||||
],
|
||||
"headersSize": -1,
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
||||
{ "name": "Content-Encoding", "value": "br" },
|
||||
{ "name": "Content-Language", "value": "en-us" },
|
||||
{ "name": "Content-Length", "value": "851" },
|
||||
{ "name": "Content-Type", "value": "application/json" },
|
||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
||||
{ "name": "X-Api-Version", "value": "3" },
|
||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
||||
{ "name": "X-Version", "value": "1.14.4" }
|
||||
],
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"count\":6,\"next\":null,\"previous\":null,\"all\":[8,17,7,4,11,15],\"results\":[{\"id\":8,\"name\":\"Correspondent 2\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":3,\"value\":\"2\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":17,\"name\":\"In the Last Month\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":20,\"value\":\"created:[-1 month to now]\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":7,\"name\":\"Inbox\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"9\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":4,\"name\":\"Recently Added\",\"show_on_dashboard\":true,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[],\"owner\":\"2\",\"user_can_change\":true},{\"id\":11,\"name\":\"Tag: Another Sample Tag\",\"show_on_dashboard\":false,\"show_in_sidebar\":true,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":6,\"value\":\"4\"}],\"owner\":\"2\",\"user_can_change\":true},{\"id\":15,\"name\":\"View ASN not empty\",\"show_on_dashboard\":false,\"show_in_sidebar\":false,\"sort_field\":\"created\",\"sort_reverse\":true,\"filter_rules\":[{\"rule_type\":18,\"value\":\"false\"}],\"owner\":\"2\",\"user_can_change\":true}]}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
"redirectURL": ""
|
||||
},
|
||||
"cache": {},
|
||||
"timings": { "send": -1, "wait": -1, "receive": 1.082 }
|
||||
},
|
||||
{
|
||||
"startedDateTime": "2023-05-14T07:18:59.990Z",
|
||||
"time": 0.647,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/tasks/",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Accept", "value": "application/json; version=3" },
|
||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
||||
{ "name": "Accept-Language", "value": "en-US" },
|
||||
{ "name": "Connection", "value": "keep-alive" },
|
||||
{ "name": "Host", "value": "localhost:8000" },
|
||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": -1,
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
||||
{ "name": "Content-Language", "value": "en-us" },
|
||||
{ "name": "Content-Length", "value": "2" },
|
||||
{ "name": "Content-Type", "value": "application/json" },
|
||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
||||
{ "name": "X-Api-Version", "value": "3" },
|
||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
||||
{ "name": "X-Version", "value": "1.14.4" }
|
||||
],
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "[]"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
"redirectURL": ""
|
||||
},
|
||||
"cache": {},
|
||||
"timings": { "send": -1, "wait": -1, "receive": 0.647 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,25 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import path from 'node:path'
|
||||
|
||||
const REQUESTS_HAR = path.join(__dirname, 'requests/api-settings.har')
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.route('**/api/status/', (route) =>
|
||||
route.fulfill({
|
||||
json: {
|
||||
database: { status: 'OK' },
|
||||
tasks: {
|
||||
redis_status: 'DISABLED',
|
||||
celery_status: 'DISABLED',
|
||||
index_status: 'OK',
|
||||
classifier_status: 'OK',
|
||||
sanity_check_status: 'OK',
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test('should activate / deactivate save button when settings change', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/settings')
|
||||
await expect(page.getByRole('button', { name: 'Save' })).toBeDisabled()
|
||||
await page.getByLabel('Use system setting').click()
|
||||
@@ -15,7 +28,6 @@ test('should activate / deactivate save button when settings change', async ({
|
||||
})
|
||||
|
||||
test('should warn on unsaved changes', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/settings')
|
||||
await page.getByLabel('Use system setting').click()
|
||||
await page.getByRole('link', { name: 'Dashboard' }).click()
|
||||
@@ -27,7 +39,6 @@ test('should warn on unsaved changes', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should apply appearance changes when set', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/settings')
|
||||
await expect(page.locator('html')).toHaveAttribute('data-bs-theme', /auto/)
|
||||
await page.getByLabel('Use system setting').click()
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
"""Start a disposable Paperless instance for the Playwright test suite."""
|
||||
|
||||
# ruff: noqa: INP001, T201
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import logging.config
|
||||
import os
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
|
||||
SOURCE_ROOT = REPOSITORY_ROOT / "src"
|
||||
|
||||
|
||||
def configure_environment(instance_root: Path) -> None:
|
||||
paths = {
|
||||
"PAPERLESS_CONSUMPTION_DIR": instance_root / "consume",
|
||||
"PAPERLESS_DATA_DIR": instance_root / "data",
|
||||
"PAPERLESS_MEDIA_ROOT": instance_root / "media",
|
||||
"PAPERLESS_SCRATCH_DIR": instance_root / "scratch",
|
||||
}
|
||||
for name, path in paths.items():
|
||||
path.mkdir(parents=True)
|
||||
os.environ[name] = str(path)
|
||||
(paths["PAPERLESS_DATA_DIR"] / "index").mkdir()
|
||||
|
||||
os.environ.update(
|
||||
{
|
||||
"DJANGO_SETTINGS_MODULE": "paperless.settings",
|
||||
"PAPERLESS_AI_ENABLED": "false",
|
||||
"PAPERLESS_CHANNELS_BACKEND": "channels.layers.InMemoryChannelLayer",
|
||||
"PAPERLESS_DEBUG": "true",
|
||||
"PAPERLESS_SECRET_KEY": "playwright-only-not-a-real-secret",
|
||||
},
|
||||
)
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
|
||||
def seed_database() -> None:
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management import call_command
|
||||
from django.utils import timezone
|
||||
|
||||
from documents.models import Correspondent
|
||||
from documents.models import Document
|
||||
from documents.models import DocumentType
|
||||
from documents.models import Note
|
||||
from documents.models import SavedView
|
||||
from documents.models import SavedViewFilterRule
|
||||
from documents.models import StoragePath
|
||||
from documents.models import Tag
|
||||
from documents.models import UiSettings
|
||||
|
||||
# Fixed credentials are safe within this disposable, localhost-only instance.
|
||||
admin = User.objects.create_superuser(
|
||||
username="playwright",
|
||||
password="playwright", # NOSONAR
|
||||
)
|
||||
User.objects.create_user(
|
||||
username="viewer",
|
||||
password="viewer", # NOSONAR
|
||||
)
|
||||
|
||||
inbox = Tag.objects.create(name="Inbox", is_inbox_tag=True, owner=admin)
|
||||
quick_filter = Tag.objects.create(name="Another Sample Tag", owner=admin)
|
||||
Tag.objects.create(name="TagWithPartial", owner=admin)
|
||||
invoice = DocumentType.objects.create(name="Invoice Test", owner=admin)
|
||||
correspondent_1 = Correspondent.objects.create(
|
||||
name="Test Correspondent 1",
|
||||
owner=admin,
|
||||
)
|
||||
correspondent_2 = Correspondent.objects.create(name="Correspondent 9", owner=admin)
|
||||
storage_path = StoragePath.objects.create(
|
||||
name="Testing 12",
|
||||
path="e2e/{created_year}/{title}",
|
||||
owner=admin,
|
||||
)
|
||||
|
||||
today = timezone.localdate()
|
||||
documents = []
|
||||
for number in range(1, 62):
|
||||
title = f"test document {number}" if number <= 9 else f"document {number}"
|
||||
content = (
|
||||
f"Playwright test content for document {number}"
|
||||
if number <= 32
|
||||
else f"Seeded content for document {number}"
|
||||
)
|
||||
created = today if number == 1 else datetime.date(2021, 1, 1)
|
||||
if number in (2, 3):
|
||||
created = datetime.date(2022, 12, 11)
|
||||
|
||||
documents.append(
|
||||
Document(
|
||||
title=title,
|
||||
content=content,
|
||||
checksum=f"{number:064x}",
|
||||
mime_type="application/pdf",
|
||||
filename=f"{number:07}.pdf",
|
||||
original_filename=f"document-{number}.pdf",
|
||||
archive_serial_number=1122 + number if number <= 6 else None,
|
||||
created=created,
|
||||
owner=admin,
|
||||
document_type=invoice if number <= 3 else None,
|
||||
correspondent=(
|
||||
correspondent_1
|
||||
if number <= 4
|
||||
else correspondent_2
|
||||
if number <= 7
|
||||
else None
|
||||
),
|
||||
storage_path=storage_path if number <= 8 else None,
|
||||
),
|
||||
)
|
||||
|
||||
Document.objects.bulk_create(documents)
|
||||
originals = Path(os.environ["PAPERLESS_MEDIA_ROOT"]) / "documents" / "originals"
|
||||
originals.mkdir(parents=True)
|
||||
thumbnails = Path(os.environ["PAPERLESS_MEDIA_ROOT"]) / "documents" / "thumbnails"
|
||||
thumbnails.mkdir(parents=True)
|
||||
sample_pdf = SOURCE_ROOT / "documents" / "tests" / "samples" / "simple.pdf"
|
||||
for document in documents:
|
||||
shutil.copyfile(sample_pdf, originals / document.filename)
|
||||
shutil.copyfile(
|
||||
SOURCE_ROOT / "documents" / "resources" / "document.webp",
|
||||
thumbnails / f"{document.pk:07}.webp",
|
||||
)
|
||||
|
||||
for document in documents[:8]:
|
||||
document.tags.add(inbox)
|
||||
documents[0].tags.add(quick_filter)
|
||||
|
||||
for number in range(1, 5):
|
||||
Note.objects.create(
|
||||
note=f"Playwright note {number}",
|
||||
document=documents[0],
|
||||
user=admin,
|
||||
)
|
||||
|
||||
inbox_view = SavedView.objects.create(
|
||||
name="Inbox",
|
||||
owner=admin,
|
||||
sort_field="created",
|
||||
sort_reverse=True,
|
||||
page_size=10,
|
||||
display_mode=SavedView.DisplayMode.TABLE,
|
||||
display_fields=["created", "title", "tag", "documenttype"],
|
||||
)
|
||||
SavedViewFilterRule.objects.create(
|
||||
saved_view=inbox_view,
|
||||
rule_type=6,
|
||||
value=str(inbox.pk),
|
||||
)
|
||||
|
||||
UiSettings.objects.create(
|
||||
user=admin,
|
||||
settings={
|
||||
"language": "",
|
||||
"bulk_edit": {"confirmation_dialogs": True, "apply_on_close": False},
|
||||
"documentListSize": 50,
|
||||
"dark_mode": {
|
||||
"use_system": True,
|
||||
"enabled": False,
|
||||
"thumb_inverted": True,
|
||||
},
|
||||
"theme": {"color": "#9fbf2f"},
|
||||
"document_details": {"native_pdf_viewer": False},
|
||||
"date_display": {"date_locale": "", "date_format": "mediumDate"},
|
||||
"comments_enabled": True,
|
||||
"slim_sidebar": False,
|
||||
"update_checking": {"enabled": False},
|
||||
"saved_views": {
|
||||
"warn_on_unsaved_change": True,
|
||||
"dashboard_views_visible_ids": [inbox_view.pk],
|
||||
"sidebar_views_visible_ids": [inbox_view.pk],
|
||||
},
|
||||
"notes_enabled": True,
|
||||
"tour_complete": True,
|
||||
},
|
||||
)
|
||||
|
||||
call_command(
|
||||
"document_index",
|
||||
"reindex",
|
||||
recreate=True,
|
||||
heap_size_mb=16,
|
||||
verbosity=0,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
started = time.monotonic()
|
||||
with tempfile.TemporaryDirectory(prefix="paperless-playwright-") as instance:
|
||||
configure_environment(Path(instance))
|
||||
|
||||
os.chdir(SOURCE_ROOT)
|
||||
print("Loading the Paperless backend...", flush=True)
|
||||
import django
|
||||
|
||||
django.setup()
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.management import call_command
|
||||
|
||||
settings.CELERY_TASK_ALWAYS_EAGER = True
|
||||
settings.CELERY_TASK_EAGER_PROPAGATES = True
|
||||
|
||||
print("Migrating the disposable database...", flush=True)
|
||||
call_command("migrate", interactive=False, verbosity=0)
|
||||
print("Seeding Playwright data...", flush=True)
|
||||
seed_database()
|
||||
elapsed = time.monotonic() - started
|
||||
print(f"Playwright backend ready in {elapsed:.1f}s", flush=True)
|
||||
settings.LOGGING["handlers"]["console"]["level"] = "WARNING"
|
||||
settings.LOGGING["handlers"]["playwright_null"] = {
|
||||
"class": "logging.NullHandler",
|
||||
}
|
||||
settings.LOGGING["loggers"]["django.server"] = {
|
||||
"handlers": ["playwright_null"],
|
||||
"propagate": False,
|
||||
}
|
||||
logging.config.dictConfig(settings.LOGGING)
|
||||
call_command(
|
||||
"runserver",
|
||||
"localhost:8001",
|
||||
use_reloader=False,
|
||||
verbosity=1,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,21 +1,13 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import path from 'node:path'
|
||||
|
||||
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-dashboard1.har')
|
||||
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-dashboard2.har')
|
||||
const REQUESTS_HAR3 = path.join(__dirname, 'requests/api-dashboard3.har')
|
||||
const REQUESTS_HAR4 = path.join(__dirname, 'requests/api-dashboard4.har')
|
||||
|
||||
test('dashboard inbox link', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await page.getByRole('link', { name: 'Documents in inbox' }).click()
|
||||
await expect(page).toHaveURL(/tags__id__in=9/)
|
||||
await expect(page).toHaveURL(/tags__id__in=1/)
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
||||
})
|
||||
|
||||
test('dashboard total documents link', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await page.getByRole('link').filter({ hasText: 'Total documents' }).click()
|
||||
await expect(page).toHaveURL(/documents/)
|
||||
@@ -24,7 +16,6 @@ test('dashboard total documents link', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('dashboard saved view show all', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR3, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await page
|
||||
.locator('pngx-widget-frame')
|
||||
@@ -32,12 +23,11 @@ test('dashboard saved view show all', async ({ page }) => {
|
||||
.getByRole('link', { name: 'Show all' })
|
||||
.first()
|
||||
.click()
|
||||
await expect(page).toHaveURL(/view\/7/)
|
||||
await expect(page).toHaveURL(/view\/1/)
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
||||
})
|
||||
|
||||
test('dashboard saved view document links', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR4, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await page
|
||||
.locator('pngx-widget-frame')
|
||||
@@ -46,11 +36,10 @@ test('dashboard saved view document links', async ({ page }) => {
|
||||
.getByRole('link', { name: /test/ })
|
||||
.first()
|
||||
.click({ position: { x: 0, y: 0 } })
|
||||
await expect(page).toHaveURL(/documents\/310\/details/)
|
||||
await expect(page).toHaveURL(/documents\/1\/details/)
|
||||
})
|
||||
|
||||
test('test slim sidebar', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await page.locator('.sidebar-slim-toggler').click()
|
||||
await expect(
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { expect, test, type WebSocketRoute } from '@playwright/test'
|
||||
import path from 'node:path'
|
||||
|
||||
const REQUESTS_HAR = path.join(__dirname, 'requests/api-document-detail.har')
|
||||
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-detail2.har')
|
||||
import { expect, test } from '@playwright/test'
|
||||
|
||||
test('should activate / deactivate save button when changes are saved', async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/documents/175/')
|
||||
await page.goto('/documents/1/')
|
||||
await page.waitForSelector('pngx-document-detail pngx-input-text:first-child')
|
||||
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
|
||||
/\w+/
|
||||
@@ -19,8 +14,7 @@ test('should activate / deactivate save button when changes are saved', async ({
|
||||
})
|
||||
|
||||
test('should warn on unsaved changes', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/documents/175/')
|
||||
await page.goto('/documents/1/')
|
||||
await expect(page.getByTitle('Correspondent', { exact: true })).toHaveText(
|
||||
/\w+/
|
||||
)
|
||||
@@ -38,28 +32,27 @@ test('should warn on unsaved changes', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should support tab direct navigation', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/documents/175/details')
|
||||
await page.goto('/documents/1/details')
|
||||
await expect(page.getByRole('tab', { name: 'Details' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
await page.goto('/documents/175/content')
|
||||
await page.goto('/documents/1/content')
|
||||
await expect(page.getByRole('tab', { name: 'Content' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
await page.goto('/documents/175/metadata')
|
||||
await page.goto('/documents/1/metadata')
|
||||
await expect(page.getByRole('tab', { name: 'Metadata' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
await page.goto('/documents/175/notes')
|
||||
await page.goto('/documents/1/notes')
|
||||
await expect(page.getByRole('tab', { name: 'Notes' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
)
|
||||
await page.goto('/documents/175/permissions')
|
||||
await page.goto('/documents/1/permissions')
|
||||
await expect(page.getByRole('tab', { name: 'Permissions' })).toHaveAttribute(
|
||||
'aria-selected',
|
||||
'true'
|
||||
@@ -67,8 +60,7 @@ test('should support tab direct navigation', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should show a mobile preview', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/documents/175/')
|
||||
await page.goto('/documents/1/')
|
||||
await page.setViewportSize({ width: 400, height: 1000 })
|
||||
await expect(page.getByRole('tab', { name: 'Preview' })).toBeVisible()
|
||||
await page.getByRole('tab', { name: 'Preview' }).click()
|
||||
@@ -76,8 +68,7 @@ test('should show a mobile preview', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should show a list of notes', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/documents/175/notes')
|
||||
await page.goto('/documents/1/notes')
|
||||
await expect(page.locator('pngx-document-notes')).toBeVisible()
|
||||
await expect(
|
||||
await page.getByRole('button', {
|
||||
@@ -88,32 +79,25 @@ test('should show a list of notes', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should support quick filters', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
|
||||
await page.goto('/documents/175/details')
|
||||
await page.goto('/documents/1/details')
|
||||
await page
|
||||
.getByRole('button', { name: 'Filter documents with these Tags' })
|
||||
.click()
|
||||
await expect(page).toHaveURL(/tags__id__all=4&sort=created&reverse=1&page=1/)
|
||||
await expect(page).toHaveURL(
|
||||
/tags__id__all=2,1&sort=created&reverse=1&page=1/
|
||||
)
|
||||
})
|
||||
|
||||
test('should finish reloading the preview after a remote document update', async ({
|
||||
page,
|
||||
}) => {
|
||||
let resolveStatusSocket: (socket: WebSocketRoute) => void
|
||||
const statusSocketReady = new Promise<WebSocketRoute>((resolve) => {
|
||||
resolveStatusSocket = resolve
|
||||
})
|
||||
await page.routeWebSocket(/\/ws\/status\/$/, (socket) => {
|
||||
resolveStatusSocket(socket)
|
||||
})
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
let previewRequestCount = 0
|
||||
page.on('request', (request) => {
|
||||
if (request.url().includes('/api/documents/175/preview/')) {
|
||||
if (request.url().includes('/api/documents/1/preview/')) {
|
||||
previewRequestCount++
|
||||
}
|
||||
})
|
||||
await page.goto('/documents/175/details')
|
||||
await page.goto('/documents/1/details')
|
||||
|
||||
await page.locator('pngx-document-detail').waitFor()
|
||||
await expect(page.getByTitle('Storage path', { exact: true })).toHaveText(
|
||||
@@ -128,27 +112,37 @@ test('should finish reloading the preview after a remote document update', async
|
||||
expect(previewWasLoaded).toBe(true)
|
||||
const previewRequestsBeforeReload = previewRequestCount
|
||||
|
||||
const statusSocket = await statusSocketReady
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => {
|
||||
const detail = document.querySelector('pngx-document-detail')
|
||||
return (window as any).ng.getComponent(detail).networkActive()
|
||||
})
|
||||
)
|
||||
.toBe(false)
|
||||
const documentReloaded = page.waitForResponse(
|
||||
(response) =>
|
||||
response.url().includes('/api/documents/175/?full_perms=true') &&
|
||||
response.url().includes('/api/documents/1/?full_perms=true') &&
|
||||
response.request().method() === 'GET'
|
||||
)
|
||||
statusSocket.send(
|
||||
JSON.stringify({
|
||||
type: 'document_updated',
|
||||
data: {
|
||||
document_id: 175,
|
||||
modified: '2026-07-26T20:00:00Z',
|
||||
},
|
||||
await page.evaluate(() => {
|
||||
const detail = document.querySelector('pngx-document-detail')
|
||||
const component = (window as any).ng.getComponent(detail)
|
||||
component.handleIncomingDocumentUpdated({
|
||||
document_id: 1,
|
||||
modified: '2099-07-26T20:00:00Z',
|
||||
})
|
||||
)
|
||||
})
|
||||
await documentReloaded
|
||||
|
||||
await expect(
|
||||
page.getByText('Document reloaded with latest changes.').first()
|
||||
).toBeVisible()
|
||||
await expect
|
||||
.poll(() => previewRequestCount)
|
||||
.toBeGreaterThan(previewRequestsBeforeReload + 1)
|
||||
.toBeGreaterThan(previewRequestsBeforeReload)
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(() => {
|
||||
const detail = document.querySelector('pngx-document-detail')
|
||||
return (window as any).ng.getComponent(detail).previewLoaded()
|
||||
})
|
||||
)
|
||||
.toBe(true)
|
||||
})
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import path from 'node:path'
|
||||
|
||||
const REQUESTS_HAR1 = path.join(__dirname, 'requests/api-document-list1.har')
|
||||
const REQUESTS_HAR2 = path.join(__dirname, 'requests/api-document-list2.har')
|
||||
const REQUESTS_HAR3 = path.join(__dirname, 'requests/api-document-list3.har')
|
||||
const REQUESTS_HAR4 = path.join(__dirname, 'requests/api-document-list4.har')
|
||||
const REQUESTS_HAR5 = path.join(__dirname, 'requests/api-document-list5.har')
|
||||
const REQUESTS_HAR6 = path.join(__dirname, 'requests/api-document-list6.har')
|
||||
|
||||
test('basic filtering', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR1, { notFound: 'fallback' })
|
||||
await page.goto('/documents')
|
||||
await page.getByRole('button', { name: 'Tags' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Inbox' }).click()
|
||||
await expect(page).toHaveURL(/tags__id__all=9/)
|
||||
await expect(page).toHaveURL(/tags__id__all=1/)
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
||||
await page.getByRole('button', { name: 'Document type' }).click()
|
||||
await page.getByRole('menuitem', { name: /^Invoice Test/ }).click()
|
||||
@@ -23,28 +14,27 @@ test('basic filtering', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Correspondent' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Test Correspondent 1' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Correspondent 9' }).click()
|
||||
await expect(page).toHaveURL(/correspondent__id__in=12,1/)
|
||||
await expect(page).toHaveURL(/correspondent__id__in=(?:1,2|2,1)/)
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/7 documents/)
|
||||
await page
|
||||
.locator('pngx-filter-editor')
|
||||
.getByTitle('Correspondent')
|
||||
.getByText('Exclude')
|
||||
.click()
|
||||
await expect(page).toHaveURL(/correspondent__id__none=12,1/)
|
||||
await expect(page).toHaveURL(/correspondent__id__none=(?:1,2|2,1)/)
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/54 documents/)
|
||||
// clear button
|
||||
await page.getByRole('button', { name: '2 selected', exact: true }).click()
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/61 documents/)
|
||||
await page.getByRole('button', { name: 'Storage path' }).click()
|
||||
await page.getByRole('menuitem', { name: 'Testing 12' }).click()
|
||||
await expect(page).toHaveURL(/storage_path__id__in=5/)
|
||||
await expect(page).toHaveURL(/storage_path__id__in=1/)
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/8 documents/)
|
||||
await page.getByRole('button', { name: 'Reset filters' }).first().click()
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/61 documents/)
|
||||
})
|
||||
|
||||
test('text filtering', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR2, { notFound: 'fallback' })
|
||||
await page.goto('/documents')
|
||||
await page.getByRole('main').getByRole('combobox').click()
|
||||
await page.getByRole('main').getByRole('combobox').fill('test')
|
||||
@@ -57,7 +47,7 @@ test('text filtering', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Title', exact: true }).click()
|
||||
await page.getByRole('button', { name: 'Advanced search' }).click()
|
||||
await expect(page).toHaveURL(/query=test/)
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/26 documents/)
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/32 documents/)
|
||||
await page.getByRole('button', { name: 'Advanced search' }).click()
|
||||
await page.getByRole('button', { name: 'ASN' }).click()
|
||||
await page.getByRole('main').getByRole('combobox').nth(1).fill('1123')
|
||||
@@ -80,7 +70,6 @@ test('text filtering', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('date filtering', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR3, { notFound: 'fallback' })
|
||||
await page.goto('/documents')
|
||||
await page.getByRole('button', { name: 'Dates' }).click()
|
||||
await page.locator('.ng-arrow-wrapper').first().click()
|
||||
@@ -94,15 +83,14 @@ test('date filtering', async ({ page }) => {
|
||||
await page.getByRole('option', { name: 'Within 3 months' }).click()
|
||||
await page.getByLabel('Dates selected').locator('button').first().click()
|
||||
await page.getByLabel('Dates selected').locator('button').first().click()
|
||||
await page.getByRole('combobox', { name: 'Select month' }).selectOption('12')
|
||||
await page.getByRole('combobox', { name: 'Select year' }).selectOption('2022')
|
||||
await page.getByText('11', { exact: true }).click()
|
||||
const createdFrom = page.getByRole('textbox', { name: 'mm/dd/yyyy' }).first()
|
||||
await createdFrom.fill('12/11/2022')
|
||||
await createdFrom.press('Enter')
|
||||
await page.getByRole('button', { name: 'Title & content' }).click()
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/2 documents/)
|
||||
await expect(page.locator('pngx-document-list')).toHaveText(/3 documents/)
|
||||
})
|
||||
|
||||
test('sorting', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR4, { notFound: 'fallback' })
|
||||
await page.goto('/documents')
|
||||
await page.getByRole('button', { name: 'Sort' }).click()
|
||||
await page.getByRole('button', { name: 'ASN' }).click()
|
||||
@@ -140,7 +128,6 @@ test('sorting', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('change views', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR5, { notFound: 'fallback' })
|
||||
await page.goto('/documents')
|
||||
await page.locator('.btn-group > label').first().click()
|
||||
await expect(page.locator('pngx-document-list table')).toBeVisible()
|
||||
@@ -151,12 +138,18 @@ test('change views', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('bulk edit', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR6, { notFound: 'fallback' })
|
||||
const uiSettingsLoaded = page.waitForResponse(
|
||||
(response) => response.url().includes('/api/ui_settings/') && response.ok()
|
||||
)
|
||||
await page.goto('/documents')
|
||||
await uiSettingsLoaded
|
||||
|
||||
await page.locator('pngx-document-card-small').nth(0).click()
|
||||
await page
|
||||
.locator('pngx-document-card-small')
|
||||
.locator('pngx-document-card-small .doc-img-container')
|
||||
.nth(0)
|
||||
.click()
|
||||
await page
|
||||
.locator('pngx-document-card-small .doc-img-container')
|
||||
.nth(3)
|
||||
.click({
|
||||
modifiers: ['Shift'],
|
||||
@@ -176,8 +169,14 @@ test('bulk edit', async ({ page }) => {
|
||||
)
|
||||
await page.getByRole('button', { name: 'None' }).click()
|
||||
|
||||
await page.locator('pngx-document-card-small').nth(1).click()
|
||||
await page.locator('pngx-document-card-small').nth(2).click()
|
||||
await page
|
||||
.locator('pngx-document-card-small .doc-img-container')
|
||||
.nth(1)
|
||||
.click()
|
||||
await page
|
||||
.locator('pngx-document-card-small .doc-img-container')
|
||||
.nth(2)
|
||||
.click()
|
||||
|
||||
await page.getByRole('button', { name: 'Tags' }).click()
|
||||
await page
|
||||
@@ -185,15 +184,26 @@ test('bulk edit', async ({ page }) => {
|
||||
.fill('TagWithPartial')
|
||||
await page.getByRole('menuitem', { name: 'TagWithPartial' }).click()
|
||||
|
||||
await page.getByRole('button', { name: 'Apply' }).click()
|
||||
|
||||
const bulkEditPromise = page.waitForRequest((request) => {
|
||||
if (
|
||||
!request.url().includes('/api/documents/bulk_edit/') ||
|
||||
request.method() !== 'POST'
|
||||
) {
|
||||
return false
|
||||
}
|
||||
const postData = request.postDataJSON()
|
||||
let isValid = postData['method'] == 'modify_tags'
|
||||
isValid = isValid && postData['parameters']['add_tags'].includes(5)
|
||||
return request.url().toString().includes('bulk_edit') && isValid
|
||||
isValid =
|
||||
isValid &&
|
||||
[
|
||||
...postData['parameters']['add_tags'],
|
||||
...postData['parameters']['remove_tags'],
|
||||
].includes(3)
|
||||
return isValid
|
||||
})
|
||||
|
||||
await page.getByRole('button', { name: 'Apply' }).click()
|
||||
await expect(page.getByRole('button', { name: 'Confirm' })).toBeVisible()
|
||||
await page.getByRole('button', { name: 'Confirm' }).click()
|
||||
await bulkEditPromise
|
||||
})
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { expect, test } from '@playwright/test'
|
||||
import path from 'node:path'
|
||||
|
||||
const REQUESTS_HAR = path.join(__dirname, 'requests/api-global-permissions.har')
|
||||
test.use({
|
||||
extraHTTPHeaders: {
|
||||
Authorization: `Basic ${Buffer.from('viewer:viewer').toString('base64')}`,
|
||||
},
|
||||
})
|
||||
|
||||
test('should not allow user to edit settings', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await expect(page.getByRole('link', { name: 'Settings' })).not.toBeAttached()
|
||||
await page.goto('/settings')
|
||||
@@ -14,7 +16,6 @@ test('should not allow user to edit settings', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should not allow user to view documents', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await expect(
|
||||
page.locator('nav').getByRole('link', { name: 'Documents' })
|
||||
@@ -30,7 +31,6 @@ test('should not allow user to view documents', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should not allow user to view correspondents', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Attributes' })
|
||||
@@ -42,7 +42,6 @@ test('should not allow user to view correspondents', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should not allow user to view tags', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Attributes' })
|
||||
@@ -54,7 +53,6 @@ test('should not allow user to view tags', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should not allow user to view document types', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Attributes' })
|
||||
@@ -66,7 +64,6 @@ test('should not allow user to view document types', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should not allow user to view storage paths', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await expect(
|
||||
page.getByRole('link', { name: 'Attributes' })
|
||||
@@ -78,7 +75,6 @@ test('should not allow user to view storage paths', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should not allow user to view logs', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await expect(page.getByRole('link', { name: 'Logs' })).not.toBeAttached()
|
||||
await page.goto('/logs')
|
||||
@@ -88,7 +84,6 @@ test('should not allow user to view logs', async ({ page }) => {
|
||||
})
|
||||
|
||||
test('should not allow user to view tasks', async ({ page }) => {
|
||||
await page.routeFromHAR(REQUESTS_HAR, { notFound: 'fallback' })
|
||||
await page.goto('/dashboard')
|
||||
await expect(page.getByRole('link', { name: 'Tasks' })).not.toBeAttached()
|
||||
await page.goto('/tasks')
|
||||
|
||||
@@ -1,353 +0,0 @@
|
||||
{
|
||||
"log": {
|
||||
"version": "1.2",
|
||||
"creator": {
|
||||
"name": "Playwright",
|
||||
"version": "1.33.0"
|
||||
},
|
||||
"browser": {
|
||||
"name": "chromium",
|
||||
"version": "113.0.5672.53"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"startedDateTime": "2023-05-14T07:16:51.455Z",
|
||||
"time": 5.787,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/ui_settings/",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Accept", "value": "application/json; version=3" },
|
||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
||||
{ "name": "Accept-Language", "value": "en-US" },
|
||||
{ "name": "Connection", "value": "keep-alive" },
|
||||
{ "name": "Host", "value": "localhost:8000" },
|
||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": -1,
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
||||
{ "name": "Content-Encoding", "value": "br" },
|
||||
{ "name": "Content-Language", "value": "en-us" },
|
||||
{ "name": "Content-Length", "value": "385" },
|
||||
{ "name": "Content-Type", "value": "application/json" },
|
||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
||||
{ "name": "X-Api-Version", "value": "3" },
|
||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
||||
{ "name": "X-Version", "value": "1.14.4" }
|
||||
],
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[]}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
"redirectURL": ""
|
||||
},
|
||||
"cache": {},
|
||||
"timings": { "send": -1, "wait": -1, "receive": 5.787 }
|
||||
},
|
||||
{
|
||||
"startedDateTime": "2023-05-14T07:16:51.578Z",
|
||||
"time": 0.566,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/tasks/",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Accept", "value": "application/json; version=3" },
|
||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
||||
{ "name": "Accept-Language", "value": "en-US" },
|
||||
{ "name": "Connection", "value": "keep-alive" },
|
||||
{ "name": "Host", "value": "localhost:8000" },
|
||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": -1,
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
||||
{ "name": "Content-Language", "value": "en-us" },
|
||||
{ "name": "Content-Length", "value": "2" },
|
||||
{ "name": "Content-Type", "value": "application/json" },
|
||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
||||
{ "name": "X-Api-Version", "value": "3" },
|
||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
||||
{ "name": "X-Version", "value": "1.14.4" }
|
||||
],
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "[]"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
"redirectURL": ""
|
||||
},
|
||||
"cache": {},
|
||||
"timings": { "send": -1, "wait": -1, "receive": 0.566 }
|
||||
},
|
||||
{
|
||||
"startedDateTime": "2023-05-14T07:16:51.578Z",
|
||||
"time": 0.452,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/statistics/",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Accept", "value": "application/json; version=3" },
|
||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
||||
{ "name": "Accept-Language", "value": "en-US" },
|
||||
{ "name": "Connection", "value": "keep-alive" },
|
||||
{ "name": "Host", "value": "localhost:8000" },
|
||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": -1,
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
||||
{ "name": "Content-Language", "value": "en-us" },
|
||||
{ "name": "Content-Length", "value": "257" },
|
||||
{ "name": "Content-Type", "value": "application/json" },
|
||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
||||
{ "name": "X-Api-Version", "value": "3" },
|
||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
||||
{ "name": "X-Version", "value": "1.14.4" }
|
||||
],
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tag\":9,\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
"redirectURL": ""
|
||||
},
|
||||
"cache": {},
|
||||
"timings": { "send": -1, "wait": -1, "receive": 0.452 }
|
||||
},
|
||||
{
|
||||
"startedDateTime": "2023-05-14T07:16:51.691Z",
|
||||
"time": 0.891,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/ui_settings/",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Accept", "value": "application/json; version=3" },
|
||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
||||
{ "name": "Accept-Language", "value": "en-US" },
|
||||
{ "name": "Connection", "value": "keep-alive" },
|
||||
{ "name": "Host", "value": "localhost:8000" },
|
||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": -1,
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Allow", "value": "GET, POST, HEAD, OPTIONS" },
|
||||
{ "name": "Content-Encoding", "value": "br" },
|
||||
{ "name": "Content-Language", "value": "en-us" },
|
||||
{ "name": "Content-Length", "value": "385" },
|
||||
{ "name": "Content-Type", "value": "application/json" },
|
||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie, Accept-Encoding" },
|
||||
{ "name": "X-Api-Version", "value": "3" },
|
||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
||||
{ "name": "X-Version", "value": "1.14.4" }
|
||||
],
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"user\":{\"id\":2,\"username\":\"testuser\",\"is_superuser\":false,\"groups\":[]},\"settings\":{\"language\":\"\",\"bulk_edit\":{\"confirmation_dialogs\":true,\"apply_on_close\":false},\"documentListSize\":50,\"dark_mode\":{\"use_system\":false,\"enabled\":\"false\",\"thumb_inverted\":\"true\"},\"theme\":{\"color\":\"#9fbf2f\"},\"document_details\":{\"native_pdf_viewer\":false},\"date_display\":{\"date_locale\":\"\",\"date_format\":\"mediumDate\"},\"notifications\":{\"consumer_new_documents\":true,\"consumer_success\":true,\"consumer_failed\":true,\"consumer_suppress_on_dashboard\":true},\"comments_enabled\":true,\"slim_sidebar\":false,\"update_checking\":{\"enabled\":false,\"backend_setting\":\"default\"},\"saved_views\":{\"warn_on_unsaved_change\":true},\"notes_enabled\":true,\"tour_complete\":true},\"permissions\":[]}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
"redirectURL": ""
|
||||
},
|
||||
"cache": {},
|
||||
"timings": { "send": -1, "wait": -1, "receive": 0.891 }
|
||||
},
|
||||
{
|
||||
"startedDateTime": "2023-05-14T07:16:51.739Z",
|
||||
"time": 0.405,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/tasks/",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Accept", "value": "application/json; version=3" },
|
||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
||||
{ "name": "Accept-Language", "value": "en-US" },
|
||||
{ "name": "Connection", "value": "keep-alive" },
|
||||
{ "name": "Host", "value": "localhost:8000" },
|
||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": -1,
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
||||
{ "name": "Content-Language", "value": "en-us" },
|
||||
{ "name": "Content-Length", "value": "2" },
|
||||
{ "name": "Content-Type", "value": "application/json" },
|
||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
||||
{ "name": "X-Api-Version", "value": "3" },
|
||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
||||
{ "name": "X-Version", "value": "1.14.4" }
|
||||
],
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "[]"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
"redirectURL": ""
|
||||
},
|
||||
"cache": {},
|
||||
"timings": { "send": -1, "wait": -1, "receive": 0.405 }
|
||||
},
|
||||
{
|
||||
"startedDateTime": "2023-05-14T07:16:51.739Z",
|
||||
"time": 0.665,
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "http://localhost:8000/api/statistics/",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Accept", "value": "application/json; version=3" },
|
||||
{ "name": "Accept-Encoding", "value": "gzip, deflate, br" },
|
||||
{ "name": "Accept-Language", "value": "en-US" },
|
||||
{ "name": "Connection", "value": "keep-alive" },
|
||||
{ "name": "Host", "value": "localhost:8000" },
|
||||
{ "name": "Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Referer", "value": "http://localhost:4200/" },
|
||||
{ "name": "Sec-Fetch-Dest", "value": "empty" },
|
||||
{ "name": "Sec-Fetch-Mode", "value": "cors" },
|
||||
{ "name": "Sec-Fetch-Site", "value": "same-site" },
|
||||
{ "name": "User-Agent", "value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.5672.53 Safari/537.36" }
|
||||
],
|
||||
"queryString": [],
|
||||
"headersSize": -1,
|
||||
"bodySize": -1
|
||||
},
|
||||
"response": {
|
||||
"status": 200,
|
||||
"statusText": "OK",
|
||||
"httpVersion": "HTTP/1.1",
|
||||
"cookies": [],
|
||||
"headers": [
|
||||
{ "name": "Access-Control-Allow-Origin", "value": "http://localhost:4200" },
|
||||
{ "name": "Allow", "value": "GET, HEAD, OPTIONS" },
|
||||
{ "name": "Content-Language", "value": "en-us" },
|
||||
{ "name": "Content-Length", "value": "257" },
|
||||
{ "name": "Content-Type", "value": "application/json" },
|
||||
{ "name": "Cross-Origin-Opener-Policy", "value": "same-origin" },
|
||||
{ "name": "Referrer-Policy", "value": "same-origin" },
|
||||
{ "name": "Vary", "value": "Accept, Accept-Language, Origin, Cookie" },
|
||||
{ "name": "X-Api-Version", "value": "3" },
|
||||
{ "name": "X-Content-Type-Options", "value": "nosniff" },
|
||||
{ "name": "X-Frame-Options", "value": "ANY" },
|
||||
{ "name": "X-Version", "value": "1.14.4" }
|
||||
],
|
||||
"content": {
|
||||
"size": -1,
|
||||
"mimeType": "application/json",
|
||||
"text": "{\"documents_total\":61,\"documents_inbox\":8,\"inbox_tag\":9,\"document_file_type_counts\":[{\"mime_type\":\"application/pdf\",\"mime_type_count\":57},{\"mime_type\":\"text/plain\",\"mime_type_count\":3},{\"mime_type\":\"text/csv\",\"mime_type_count\":1}],\"character_count\":2407053}"
|
||||
},
|
||||
"headersSize": -1,
|
||||
"bodySize": -1,
|
||||
"redirectURL": ""
|
||||
},
|
||||
"cache": {},
|
||||
"timings": { "send": -1, "wait": -1, "receive": 0.665 }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build",
|
||||
"e2e": "playwright test",
|
||||
"e2e:backend": "uv run --project .. --no-sync python e2e/backend.py",
|
||||
"e2e:ui": "playwright test --ui",
|
||||
"start:e2e": "ng serve --configuration=e2e",
|
||||
"test": "ng test",
|
||||
"lint": "ng lint"
|
||||
},
|
||||
|
||||
@@ -14,17 +14,25 @@ export default defineConfig({
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 3 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Keep parallelism modest for the shared SQLite backend. */
|
||||
workers: 2,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
port,
|
||||
command: 'pnpm run start',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 2 * 60 * 1000,
|
||||
},
|
||||
/* Run the disposable backend and local UI before starting the tests. */
|
||||
webServer: [
|
||||
{
|
||||
url: 'http://localhost:8001/accounts/login/',
|
||||
command: 'npm run e2e:backend',
|
||||
reuseExistingServer: false,
|
||||
timeout: 2 * 60 * 1000,
|
||||
},
|
||||
{
|
||||
port,
|
||||
command: 'npm run start:e2e',
|
||||
reuseExistingServer: !process.env.CI,
|
||||
timeout: 2 * 60 * 1000,
|
||||
},
|
||||
],
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<div class="d-flex flex-wrap flex-row gap-2 w-100 mh-1" style="min-height: 1em;"
|
||||
cdkDropList #selectedList="cdkDropList"
|
||||
cdkDropListOrientation="mixed"
|
||||
[cdkDropListDisabled]="disabled"
|
||||
(cdkDropListDropped)="drop($event)"
|
||||
[cdkDropListConnectedTo]="[unselectedList]">
|
||||
@for (item of selectedItems; track item.id) {
|
||||
@@ -17,6 +18,7 @@
|
||||
<div class="d-flex flex-wrap flex-row gap-2 w-100" style="min-height: 1em;"
|
||||
cdkDropList #unselectedList="cdkDropList"
|
||||
cdkDropListOrientation="mixed"
|
||||
[cdkDropListDisabled]="disabled"
|
||||
(cdkDropListDropped)="drop($event)"
|
||||
[cdkDropListConnectedTo]="[selectedList]">
|
||||
@for (item of unselectedItems; track item.id) {
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
.badge {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.cdk-drop-list-disabled {
|
||||
cursor: not-allowed !important;
|
||||
|
||||
* {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +98,29 @@ describe('DragDropSelectComponent', () => {
|
||||
{ id: '3', name: 'Item 3' },
|
||||
])
|
||||
})
|
||||
|
||||
it('should disable drag and drop when the control is disabled', () => {
|
||||
component.items = [
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '2', name: 'Item 2' },
|
||||
]
|
||||
component.writeValue(['1', '2'])
|
||||
component.setDisabledState(true)
|
||||
fixture.detectChanges()
|
||||
|
||||
expect(component.selectedList.disabled).toBe(true)
|
||||
expect(component.unselectedList.disabled).toBe(true)
|
||||
|
||||
component.drop({
|
||||
previousContainer: component.selectedList,
|
||||
container: component.selectedList,
|
||||
previousIndex: 0,
|
||||
currentIndex: 1,
|
||||
} as any)
|
||||
|
||||
expect(component.selectedItems).toEqual([
|
||||
{ id: '1', name: 'Item 1' },
|
||||
{ id: '2', name: 'Item 2' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -46,6 +46,8 @@ export class DragDropSelectComponent extends AbstractInputComponent<string[]> {
|
||||
}
|
||||
|
||||
public drop(event: CdkDragDrop<string[]>) {
|
||||
if (this.disabled) return
|
||||
|
||||
if (
|
||||
event.previousContainer === event.container &&
|
||||
event.container === this.selectedList
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</div>
|
||||
<div class="d-none d-sm-flex flex-fill me-3">
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text border-0" i18n>Select:</span>
|
||||
<span class="input-group-text bg-transparent border-0" i18n>Select:</span>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm flex-nowrap">
|
||||
@if (list.hasSelection) {
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
</div>
|
||||
<div class="col">
|
||||
<label class="form-label" for="display_mode_{{view.id}}" i18n>Display as</label>
|
||||
<select class="form-select" formControlName="display_mode">
|
||||
<select class="form-select form-control" formControlName="display_mode">
|
||||
<option [ngValue]="DisplayMode.TABLE" i18n>Table</option>
|
||||
<option [ngValue]="DisplayMode.SMALL_CARDS" i18n>Small Cards</option>
|
||||
<option [ngValue]="DisplayMode.LARGE_CARDS" i18n>Large Cards</option>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export const DEFAULT_APP_TITLE = 'Paperless-ngx'
|
||||
|
||||
export const environment = {
|
||||
production: false,
|
||||
apiBaseUrl: 'http://localhost:8001/api/',
|
||||
apiVersion: '10',
|
||||
appTitle: DEFAULT_APP_TITLE,
|
||||
tag: 'e2e',
|
||||
version: 'E2E',
|
||||
webSocketHost: 'localhost:8001',
|
||||
webSocketProtocol: 'ws:',
|
||||
webSocketBaseUrl: '/ws/',
|
||||
}
|
||||
@@ -103,6 +103,8 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,%3csvg xmlns='h
|
||||
--bs-danger-rgb: 183, 22, 49;
|
||||
--bs-body-bg: #161618;
|
||||
--bs-body-bg-rgb: 22, 22, 24;
|
||||
--bs-secondary-bg: var(--pngx-bg-disabled);
|
||||
--bs-secondary-bg-rgb: 36, 37, 41;
|
||||
--bs-light: #1c1c1f;
|
||||
--bs-light-rgb: 28, 28, 31;
|
||||
--bs-info: var(--pngx-bg-alt);
|
||||
|
||||
@@ -7,9 +7,10 @@ from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
|
||||
from documents.models import Document
|
||||
from documents.tests.utils import DirectoriesMixin
|
||||
|
||||
|
||||
class TestTrashAPI(APITestCase):
|
||||
class TestTrashAPI(DirectoriesMixin, APITestCase):
|
||||
def setUp(self) -> None:
|
||||
super().setUp()
|
||||
|
||||
|
||||
@@ -409,6 +409,7 @@ class TestBulkDownloadPermissionChecksRootDocument:
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.usefixtures("_search_index")
|
||||
class TestTrashRestorePermissionBoundary:
|
||||
def test_restore_rejects_document_without_delete_permission(
|
||||
self,
|
||||
|
||||