Compare commits

...
Sign in to create a new pull request.

196 commits

Author SHA1 Message Date
theresa
f9fda55d43 Tests: Update page-models 2022-02-28 14:50:01 +01:00
theresa
f5381ea01b Tests: Update page-model 2022-02-25 17:01:08 +01:00
theresa
5d31603a99 Tests: Refactor photo-edit test 2022-02-15 16:49:26 +01:00
theresa
7f66c73506 Tests: Refactor acceptance tests 2022-01-28 17:06:40 +01:00
theresa
02ec17317f Tests: Refactor acceptance tests 2022-01-27 16:54:28 +01:00
theresa
9755cec3d2 Tests: Refactor acceptance tests 2022-01-27 16:43:43 +01:00
theresa
c98bfe9493 Tests: Update page-model 2022-01-27 12:50:58 +01:00
theresa
1dfc052d0a Tests: Refactor acceptance tests 2022-01-27 12:50:39 +01:00
theresa
6c8ec3eb3f Tests: Refactor acceptance tests 2022-01-26 18:15:27 +01:00
theresa
c86569f34e Tests: Update page models 2022-01-26 18:15:07 +01:00
theresa
21aa828a20 Tests: Change class for acceptance tests 2022-01-26 18:14:51 +01:00
theresa
48a68d0ec9 Tests: Refactor acceptance tests 2022-01-25 19:12:17 +01:00
theresa
9cd87a57e0 Tests: Update page models 2022-01-25 19:01:27 +01:00
theresa
8d11f4ad8d Tests: Refactor acceptance tests 2022-01-25 17:01:50 +01:00
theresa
c2451b9892 Tests: Update page models 2022-01-25 17:01:27 +01:00
theresa
ed2c9d2883 Tests: Refactor acceptance tests 2022-01-25 12:26:18 +01:00
theresa
9701817b02 Tests: Refactor acceptance tests 2022-01-25 11:37:54 +01:00
theresa
812ffac1e3 Tests: Add page model for album dialog 2022-01-25 11:37:35 +01:00
theresa
dd8a242710 Tests: Refactor albums tests 2022-01-25 08:46:06 +01:00
theresa
9ea55e6cd9 Tests: Refactor acceptance tests 2022-01-24 16:48:38 +01:00
theresa
ce61e97c00 Tests: Refactor acceptance tests 2022-01-24 16:25:32 +01:00
theresa
537f64cb9e Tests: Update page models 2022-01-24 15:49:15 +01:00
theresa
86f6cf0de9 Tests: Refactor acceptance-tests 2022-01-24 12:53:55 +01:00
theresa
9ccf5f634a Tests: Update page models 2022-01-24 12:53:24 +01:00
theresa
ff31f401be Tests: Refactor acceptance-private tests 2022-01-21 17:55:59 +01:00
theresa
2c0fdf89d6 Tests: Update page-models 2022-01-21 17:55:27 +01:00
theresa
e61a8af05c Tests: Refactor acceptance tests 2022-01-21 17:55:03 +01:00
theresa
1b5b88a66f Tests: Refactor acceptance tests 2022-01-21 17:54:07 +01:00
theresa
7d6849fddd Tests: Refactor album tests 2022-01-21 17:53:07 +01:00
theresa
c61d045811 Tests: Refactored settings tests 2022-01-21 17:52:20 +01:00
theresa
65e4ba4eea Tests: Refactored library tests 2022-01-21 17:51:54 +01:00
theresa
e5b70a5fb2 Tests: Changed class for tests 2022-01-21 17:51:19 +01:00
theresa
df9d8c7bd2 Tests: Refactor acceptance tests 2022-01-19 18:52:36 +01:00
theresa
0654acdb78 Tests: Update page-models 2022-01-19 18:50:03 +01:00
theresa
7829dd1803 Tests: Remove logger from page models 2022-01-19 09:14:04 +01:00
theresa
cec5686f3b Tests: Refactor acceptance-tests 2022-01-18 18:30:28 +01:00
theresa
3dc390f8cb Tests: Update subject page-model 2022-01-18 18:30:01 +01:00
theresa
3dff2efbb0 Tests: Update photoview page-model 2022-01-18 18:29:44 +01:00
theresa
e3ecfb5a94 Tests: Update photo page-model 2022-01-18 18:29:24 +01:00
theresa
f96fd688b0 Tests: Update label page-model 2022-01-18 18:29:00 +01:00
theresa
0e3d6f3ee9 Tests: Update album page-model 2022-01-18 18:28:37 +01:00
theresa
f5f836e433 Tests: Refactor acceptance tests 2022-01-17 19:35:22 +01:00
theresa
3c4486d430 Tests: Add new page models 2022-01-17 19:18:12 +01:00
theresa
b76f6712df Tests: Add classes for testing 2022-01-17 19:17:34 +01:00
Michael Mayer
373549a931 Merge remote-tracking branch 'origin/develop' into feature/oidc-v2 2021-12-20 20:48:58 +01:00
Michael Mayer
7686c730e1 Merge remote-tracking branch 'origin/develop' into feature/oidc-v2 2021-12-20 16:50:24 +01:00
Michael Mayer
cb0752da9d Merge remote-tracking branch 'origin/develop' into feature/oidc-v2 2021-12-18 17:55:52 +01:00
Michael Mayer
0f9852ace4 Docker: Upgrade base image to photoprism/develop:20211218
The repository has been renamed from development to develop.
2021-12-18 16:18:29 +01:00
Michael Mayer
5a3ed38abb Docs: Add comments and sort Makefile targets 2021-12-18 15:56:20 +01:00
Michael Mayer
068feb58fc Docker: Fix Go download URL in docker/scripts/install-go.sh 2021-12-18 15:40:30 +01:00
Michael Mayer
7b124357b4 Docker: Update photoprism/develop config to match new path names 2021-12-18 15:28:24 +01:00
Michael Mayer
2e471cf6f9 Docker: Rename photoprism/development to develop for consistency 2021-12-18 15:25:48 +01:00
Michael Mayer
25614d6678 Auth/OIDC: Update docker-compose.yml for CI & Development #98 #782 2021-12-18 15:08:21 +01:00
Michael Mayer
0207d23ec8 Docker: Remove unused photoprism/goproxy config #98 #782 2021-12-18 15:00:36 +01:00
Michael Mayer
4f5c326268 Auth/OIDC: Update Issuer & WebDAV Dummy Dockerfiles #98 #782 2021-12-18 14:58:57 +01:00
Michael Mayer
20dbb90f5f Auth/OIDC: Upgrade Traefik to 20211218 and Keycloak to v16.0.0 #98 #782 2021-12-18 14:14:56 +01:00
Michael Mayer
baece3e4af Frontend: Update package-lock.json 2021-12-18 13:57:14 +01:00
Michael Mayer
2186b923c9 Merge remote-tracking branch 'origin/develop' into feature/oidc-v2
# Conflicts:
#	docker-compose.mariadb.yml
#	docker-compose.yml
#	frontend/src/component/navigation.vue
#	frontend/src/pages/albums.vue
#	frontend/tests/acceptance/settings/general.js
#	go.mod
#	go.sum
#	internal/commands/users.go
#	internal/config/flags.go
#	internal/config/options.go
#	internal/entity/user.go
#	scripts/build.sh
#	scripts/docker/arch.sh
#	scripts/sql/init-test-databases.sql
2021-12-18 11:57:44 +01:00
theresa
a68f6c1bef Tests: Add commands for openid tests to makefile 2021-12-13 15:29:36 +01:00
theresa
d39cc9e1de Tests: Add commands for openid tests to package.json 2021-12-13 15:29:06 +01:00
theresa
4ebad81d76 Tests: Add acceptance tests 2021-12-13 15:28:20 +01:00
theresa
91c4faa048 Tests: Add acceptance tests for openid auth #782 2021-12-12 15:27:04 +01:00
Timo Volkmann
b28931d8e7 OIDC: Accept gin context in handler #782 2021-12-10 10:40:39 +01:00
Timo Volkmann
f1fe2c20d6 UI: Add method for SaveAndValidate() #98 2021-12-10 10:37:11 +01:00
Timo Volkmann
c39060b0a6 UI: Improve OIDC and UI permission checks #98 #782 2021-11-30 11:08:40 +01:00
Timo Volkmann
a60bb5f6d2 Backend: Remove old version from go.sum 2021-11-20 11:43:59 +01:00
theresa
178c160338 Tests: Improve acceptance tests for user roles #98 2021-11-16 11:22:25 +01:00
theresa
b8761aa49c Tests: Adapt tests to run on android 2021-11-12 08:53:07 +01:00
Timo Volkmann
ae882f948b Test: Improve oidc dummy OP #782 2021-11-10 18:21:50 +01:00
Timo Volkmann
9a0b4da149 Test: Update caos/oidc to 1.0.0 #782 2021-11-10 11:21:41 +01:00
Timo Volkmann
02d9578f7b Oidc: Update caos/oidc to 1.0.0 #782 2021-11-09 18:53:47 +01:00
Michael Mayer
56bf1c0b75 Auth: Upgrade caos/oidc from v0.15.12 to v1.0.0 2021-11-09 18:04:24 +01:00
Michael Mayer
7e973c3429 Merge remote-tracking branch 'origin/develop' into feature/oidc-v2 2021-11-09 17:41:08 +01:00
Michael Mayer
b8618db336 Merge remote-tracking branch 'origin/develop' into feature/oidc-v2
# Conflicts:
#	docker-compose.drone.yml
#	docker-compose.postgres.yml
2021-11-09 17:33:53 +01:00
Michael Mayer
52049c7c93 Docker: Update dummy-webdav service from 20211022 to 20211109 2021-11-09 17:25:53 +01:00
Michael Mayer
e24c4e86da Merge remote-tracking branch 'origin/develop' into feature/oidc-v2
# Conflicts:
#	docker-compose.drone.yml
#	docker-compose.postgres.yml
#	docker-compose.yml
#	go.mod
#	go.sum
2021-11-09 17:06:32 +01:00
Michael Mayer
2cd832b6b2 Go: Upgrade from 1.17.2 to 1.17.3 2021-11-09 17:01:00 +01:00
Michael Mayer
8e6a6e6108 Backend: Upgrade manifoldco/promptui from v0.8.0 to v0.9.0 2021-11-09 17:01:00 +01:00
Michael Mayer
f69d4d0787 Frontend: Update package-lock.json 2021-11-09 17:00:43 +01:00
Michael Mayer
8d3ae13f2c Places: Normalize US & CA state names #1664 2021-11-09 17:00:43 +01:00
Nico Caprioli
4d86c8ecf6 Translations: Update Italian (#1706) 2021-11-09 16:58:55 +01:00
Michael Mayer
637a0548b2 Update bug_report.md 2021-11-09 16:58:55 +01:00
Michael Mayer
7f244c7301 Backend: Upgrade dependencies in go.mod and go.sum 2021-11-09 16:58:55 +01:00
Michael Mayer
d4e7eebb4d Frontend: Update package-lock.json 2021-11-09 16:56:16 +01:00
Michael Mayer
289bc0c5c3 People: Less strict ignore list for young children's faces #1587 2021-11-09 16:56:16 +01:00
Michael Mayer
3ec492d470 Update README.md 2021-11-09 16:56:16 +01:00
Michael Mayer
0a908ec443 Shorten README.md 2021-11-09 16:56:16 +01:00
Michael Mayer
722df90e60 Shorten SPONSORS.md 2021-11-09 16:56:16 +01:00
Michael Mayer
fc3cf33d3f Add crypto wallets to SPONSORS.md 2021-11-09 16:56:16 +01:00
Timo Volkmann
7ef6d83b28 Auth: Hide account page for external users and refer to IDP #98 2021-11-09 16:54:56 +01:00
theresa
cb151d158f Tests: Adapt acceptance tests to run on mobile 2021-11-09 15:22:12 +01:00
theresa
e107bfa8d0 Tests: Adapt acceptance tests to run on mobile 2021-11-08 19:22:19 +01:00
Timo Volkmann
16ee40501e Oidc: Improve package structure and add tests #782 2021-11-07 15:05:33 +01:00
Timo Volkmann
4eacb28d8a Oidc: Introduce external role management via OP #782 2021-11-07 15:05:33 +01:00
Timo Volkmann
1ec9fc89a9 Oidc: Improve UX #782 2021-11-07 15:05:33 +01:00
Timo Volkmann
31cdbec95c Oidc: Improve error handling #782 2021-11-07 15:05:33 +01:00
theresa
950a3e84f8 Tests: Improve acceptance tests 2021-11-05 17:34:42 +01:00
theresa
9a7c070604 Tests: Adapt acceptance tests to changes 2021-11-04 08:39:23 +01:00
Timo Volkmann
2ae440d1d1 Oidc: Improve config internals & adapt tests #782 2021-11-03 20:28:37 +01:00
Timo Volkmann
16eb023d89 Oidc: Improve automatic OIDC login #782 2021-11-03 18:14:33 +01:00
Timo Volkmann
2dce7c37e5 Oidc: Prevent internal users from logging in when OIDC is enabled #782 2021-11-03 15:29:20 +01:00
Timo Volkmann
150328b6e5 Auth: Fix redirects due missing acl information & Improve acl evaluation in UI #98 2021-11-03 13:06:11 +01:00
Timo Volkmann
f9966beaec Oidc: Automatically open OIDC login page #782 #98 2021-11-03 12:51:28 +01:00
Michael Mayer
d890c66446 Auth: Document test user credentials in docker-compose.yml #782 2021-11-02 13:26:42 +01:00
Michael Mayer
dd578b7142 Auth: Add HTTPS Reverse Proxy & Keycloak for OpenID Connect tests #782 2021-11-02 13:20:38 +01:00
Michael Mayer
55bee9871f Merge remote-tracking branch 'origin/develop' into feature/oidc-v2 2021-11-01 16:01:48 +01:00
theresa
b58da79a94 Tests: Update people tests 2021-10-27 17:34:58 +02:00
theresa
61acb8a200 Tests: Add acceptance tests for member role 2021-10-27 17:34:37 +02:00
theresa
a3e9e03e99 Tests: Add acceptance-tests for admin role 2021-10-27 17:34:11 +02:00
theresa
2c41ed3862 Tests: Update page model 2021-10-27 17:33:47 +02:00
theresa
4455f11d58 Tests: Add acceptance tests 2021-10-26 18:09:11 +02:00
theresa
dd600aabd5 Users: Add acceptance tests for admin role #98 2021-10-26 10:06:17 +02:00
Michael Mayer
f94744eee1 Config: Improve docs in docker-compose.yml files for developers 2021-10-25 18:32:08 +02:00
Michael Mayer
1b73c51d3f Config: Improve docs in docker-compose.yml example for Windows 2021-10-25 18:31:09 +02:00
Michael Mayer
aa5f20e415 Merge branch 'develop' into feature/oidc-v2
# Conflicts:
#	docker-compose.yml
2021-10-25 17:29:10 +02:00
Michael Mayer
3d75269a76 Merge branch 'develop' into feature/oidc-v2 2021-10-23 19:00:46 +02:00
Michael Mayer
1b2e60a7eb Merge branch 'develop' into feature/oidc-v2 2021-10-23 18:52:44 +02:00
Timo Volkmann
b92b590635 Auth: Remove hardcoded admin property #98 2021-10-23 12:53:40 +02:00
Timo Volkmann
d983ec7c18 Auth: Disable Webdav for non-admins #98 2021-10-23 12:42:50 +02:00
Timo Volkmann
49522c06a4 UI: Fix routing loop #98 2021-10-23 12:36:50 +02:00
Timo Volkmann
aec89b53bd UI: Improve permission checks #98 2021-10-23 12:15:42 +02:00
Timo Volkmann
e32966d43b Auth: Add admin option to 'users add & update' CLI commands #98 2021-10-23 11:44:10 +02:00
Timo Volkmann
43d54a8e9c OIDC: Improve error handling #782 2021-10-23 11:39:51 +02:00
Michael Mayer
1e654352f8 CI: Run tests when pushing to feature/* 2021-10-22 18:28:42 +02:00
Michael Mayer
ab602eda19 Docker: Add dummy services dummy-webdav & dummy-oidc for development 2021-10-22 18:07:36 +02:00
Michael Mayer
c385ec73f4 Merge branch 'develop' into feature/oidc-v2 2021-10-22 15:36:02 +02:00
Michael Mayer
20c2e0ffdd Merge branch 'develop' into feature/oidc-v2 2021-10-21 20:21:05 +02:00
Timo Volkmann
044490e6da UI: Improve permission checks for collections #98 2021-10-21 15:03:18 +02:00
Timo Volkmann
d8342d4546 UI: Improve permission checks for people #98 2021-10-21 14:51:11 +02:00
Timo Volkmann
2f11587174 UI: Add permission checks for people #98 2021-10-21 14:24:10 +02:00
Michael Mayer
39063d892d Merge branch 'develop' into feature/oidc-v2 2021-10-21 13:36:53 +02:00
Michael Mayer
039c9d9d57 Merge branch 'develop' into feature/oidc-v2
# Conflicts:
#	README.md
2021-10-21 13:12:13 +02:00
Timo Volkmann
ab910b6f57 UI: Add permission checks for labels #98 2021-10-21 11:16:10 +02:00
theresa
9f19551adf Frontend: Update lavender theme 2021-10-16 22:51:31 +02:00
theresa
7d576e4e6b Frontend: Update midnight theme 2021-10-16 22:51:31 +02:00
Michael Mayer
4b51282762 Docs: Shorten README.md 2021-10-16 22:51:31 +02:00
Michael Mayer
c836c8ea9d Docs: Add demo links to feature list 2021-10-16 22:51:31 +02:00
Michael Mayer
2e6c0e0dc0 Update README.md 2021-10-16 22:51:31 +02:00
Michael Mayer
4c91cd82d6 Update README.md 2021-10-16 22:51:31 +02:00
Michael Mayer
8bfdafdffe Update README.md 2021-10-16 22:51:31 +02:00
Michael Mayer
fdcacb58d0 Update README.md 2021-10-16 22:51:31 +02:00
Michael Mayer
2be9da7a54 Update README.md 2021-10-16 22:51:31 +02:00
Michael Mayer
76af6e9ccf Update README.md 2021-10-16 22:51:30 +02:00
Michael Mayer
7842f50728 Update README.md 2021-10-16 22:51:30 +02:00
Michael Mayer
117c3d7abb Update README.md 2021-10-16 22:51:30 +02:00
Michael Mayer
2d7a063ae4 Update README.md 2021-10-16 22:51:30 +02:00
Michael Mayer
9a4b0cadaf Update README.md 2021-10-16 22:51:30 +02:00
Michael Mayer
31381627b4 Update README.md 2021-10-16 22:51:30 +02:00
Timo Volkmann
c8c7de09d5 UI: Improve permission checks #98 2021-10-15 09:52:29 +02:00
Timo Volkmann
9811c86ee8 UI: Add permission checks (albums) #98 2021-10-14 15:24:37 +02:00
Timo Volkmann
d817a42ea2 UI: Add support for multiple actions to hasPermission() #98 2021-10-14 15:22:19 +02:00
Timo Volkmann
246d29e68a UI: Improve logging 2021-10-14 12:29:32 +02:00
Timo Volkmann
34fb9b3b73 UX: Improve navigation count layout and prepare abbreviation #1616 2021-10-14 12:29:32 +02:00
Timo Volkmann
0e09eee673 UI: Add permission checks (photos) and refactor acl evaluation for public mode #98 2021-10-14 12:29:32 +02:00
Michael Mayer
e29dfcab1c Merge branch 'develop' into feature/oidc-v2 2021-10-14 11:06:55 +02:00
Michael Mayer
0facf42981 Merge branch 'develop' into feature/oidc-v2 2021-10-13 16:19:43 +02:00
Michael Mayer
77a249667b Merge branch 'develop' into feature/oidc-v2 2021-10-12 15:14:21 +02:00
theresa
e6680d7e1f Tests: Adapt acceptance test to UI change 2021-10-12 11:04:24 +02:00
Timo Volkmann
4c5ef8841b UI: Add permission checks (settings) #98 2021-10-11 15:46:39 +02:00
Timo Volkmann
01c32ff15b UI: Move account tab to separate page #98 2021-10-11 15:10:26 +02:00
Timo Volkmann
304b52f3a1 Merge remote-tracking branch 'origin/develop' into feature/oidc-v2
# Conflicts:
#	frontend/src/component/navigation.vue
#	frontend/src/css/navigation.css
2021-10-11 14:52:31 +02:00
Timo Volkmann
1a00fed2c9 UI: Introduce frontend ACL #98 2021-10-08 16:48:55 +02:00
Timo Volkmann
d1cfa2f3d9 Merge remote-tracking branch 'origin/develop' into feature/oidc-v2
# Conflicts:
#	internal/config/flags.go
2021-10-07 17:51:10 +02:00
Timo Volkmann
b1b991da6c Merge remote-tracking branch 'origin/develop' into feature/oidc-v2 2021-10-07 09:17:39 +02:00
Timo Volkmann
ca0abb1a95 Merge remote-tracking branch 'origin/develop' into feature/oidc-v2
# Conflicts:
#	docker-compose.yml
#	go.sum
#	internal/config/flags.go
#	internal/config/options.go
2021-10-06 13:45:32 +02:00
Timo Volkmann
c660a8f9c4 Auth: Adapt tests #98 2021-10-04 21:50:44 +02:00
Timo Volkmann
1d0949dad8 Auth: Allow frontend reading permission information #98 2021-10-04 21:08:10 +02:00
Timo Volkmann
b70a4b9831 Auth: Add member role #98 2021-10-04 20:59:12 +02:00
Timo Volkmann
f81dc35b04 Auth: Improve navigation UI with Flexbox #98 2021-10-04 20:46:22 +02:00
Timo Volkmann
cc6be6ba2f Auth: Improve navigation profile card #98 2021-09-30 19:14:52 +02:00
Timo Volkmann
1eb94f9b34 Auth: Move logout button to bottom #98 2021-09-30 16:27:13 +02:00
Timo Volkmann
10a6ec8eef Auth: Append suffix to username #782
If username exists locally, append suffix to preferred username
2021-09-29 14:01:49 +02:00
Timo Volkmann
d6e81cc0b3 Merge branch 'develop' into feature/oidc-v3
# Conflicts:
#	docker-compose.yml
#	go.mod
#	go.sum
#	internal/config/flags.go
#	internal/config/options.go
2021-09-27 14:37:26 +02:00
Timo Volkmann
23f12e38ca Auth: Use Dockers DNS for OIDC tests #782 2021-09-20 20:08:28 +02:00
Timo Volkmann
8d8f0df58f Auth: Update docker-compose.yml #782 2021-09-20 19:26:58 +02:00
Timo Volkmann
e3099e0cb6 Run go mod tidy and go fmt 2021-09-20 19:23:33 +02:00
Timo Volkmann
8b00cd068c Auth: More verbose httpclient debug mode #782 2021-09-20 19:19:59 +02:00
Timo Volkmann
f278fb69c1 Auth: Improve oidc test provider #782 2021-09-20 19:18:52 +02:00
Timo Volkmann
a7400d6477 Auth: Enable pkce if available #782 2021-09-20 19:16:50 +02:00
Timo Volkmann
5bac7ba11d Auth: Improve oidc tests #782 2021-09-20 19:15:59 +02:00
Timo Volkmann
24f6041a76 Auth: Make OIDC initialization more resilient #782 2021-09-20 19:14:57 +02:00
Timo Volkmann
1ae85a0e04 Merge branch 'develop' into feature/oidc-v3
# Conflicts:
#	docker-compose.yml
#	go.sum
2021-09-20 13:58:26 +02:00
Timo Volkmann
b5ddd0d3bb Debug: Install Delve on 'make start-debug' 2021-09-09 11:53:24 +02:00
Timo Volkmann
d51f24bde9 Run "make fmt” 2021-09-09 11:34:34 +02:00
Timo Volkmann
da7035c106 Entity: Add Tests #98 2021-09-09 11:33:27 +02:00
Timo Volkmann
9bdc38be9a Auth: Add Tests #782 2021-09-09 11:33:27 +02:00
Timo Volkmann
dea527591e Tests: Add httpclient tests #98 2021-09-09 11:33:27 +02:00
Timo Volkmann
0cc9dcb2b9 Tests: Add OIDC test OP #782 2021-09-09 11:33:27 +02:00
Timo Volkmann
d6fd6cf297 Backend: Tidy go modules 2021-09-09 11:33:27 +02:00
Timo Volkmann
457085d7bd Auth: Refactor OIDC #782 2021-09-09 11:33:26 +02:00
Timo Volkmann
d7aa739d26 Auth: Return error value from CreateOrUpdateUser() #98 2021-09-08 14:35:29 +02:00
Timo Volkmann
5d26325b7d Dev: Add debug command and build flags 2021-09-08 14:32:04 +02:00
Timo Volkmann
7e754e8cfa Auth: Add simple random []byte seed #98 2021-09-08 14:21:57 +02:00
Timo Volkmann
e7db639c54 Auth: Add http.Client Logger #782 2021-09-08 12:17:44 +02:00
Timo Volkmann
6634e8a56b Auth: Add OIDC support #782 2021-09-08 12:17:44 +02:00
Timo Volkmann
aa8ebc2076 Auth: Add OIDC support #782 2021-09-08 12:17:44 +02:00
153 changed files with 12499 additions and 986 deletions

View file

@ -37,6 +37,7 @@ steps:
when:
branch:
- develop
- feature/*
- name: test
commands:
@ -177,6 +178,6 @@ steps:
---
kind: signature
hmac: 61a27b3c3142a9a1d1972f0fe00a8aa19d99623e6fea6e1d5ad44b6b20cd5cd1
hmac: c2c78e8f8a5537005233ef69ea85f0b5ce6131913037b2a44f401505b887de3f
...

View file

@ -1,7 +1,7 @@
FROM photoprism/development:20211210
FROM photoprism/develop:20211218
# Copy latest entrypoint script
COPY --chown=root:root /docker/development/entrypoint.sh /entrypoint.sh
COPY --chown=root:root /docker/develop/entrypoint.sh /entrypoint.sh
COPY --chown=root:root /docker/scripts/Makefile /root/Makefile
# Set up project directory

145
Makefile
View file

@ -22,7 +22,7 @@ else
GOTEST=go test
endif
all: dep build
all: tidy dep build
dep: dep-tensorflow dep-js dep-go
build: generate build-js build-go
install: install-bin install-assets
@ -33,42 +33,32 @@ test-api: reset-testdb run-test-api
test-short: reset-testdb run-test-short
acceptance-private-run-chromium: acceptance-private-restart acceptance-private acceptance-private-stop
acceptance-public-run-chromium: acceptance-restart acceptance acceptance-stop
acceptance-openid-run-chromium: acceptance-openid-restart acceptance-openid acceptance-openid-stop
acceptance-private-run-firefox: acceptance-private-restart acceptance-private-firefox acceptance-private-stop
acceptance-public-run-firefox: acceptance-restart acceptance-firefox acceptance-stop
acceptance-run-chromium: acceptance-private-restart acceptance-private acceptance-private-stop acceptance-restart acceptance acceptance-stop
acceptance-run-firefox: acceptance-private-restart acceptance-private-firefox acceptance-private-stop acceptance-restart acceptance-firefox acceptance-stop
acceptance-openid-run-firefox: acceptance-openid-restart acceptance-openid-firefox acceptance-openid-stop
acceptance-run-chromium: acceptance-private-restart acceptance-private acceptance-private-stop acceptance-restart acceptance acceptance-stop acceptance-openid-restart acceptance-openid acceptance-openid-stop
acceptance-run-firefox: acceptance-private-restart acceptance-private-firefox acceptance-private-stop acceptance-restart acceptance-firefox acceptance-stop acceptance-openid-restart acceptance-openid-firefox acceptance-openid-stop
test-all: test acceptance-run-chromium
fmt: fmt-js fmt-go
upgrade: dep-upgrade-js dep-upgrade
clean-local: clean-local-config clean-local-cache
clean-install: clean-local dep build-js install-bin install-assets
dev: dev-npm dev-go-amd64
dev-npm:
$(info Upgrading NPM in local dev environment...)
debug-go: build-go-remote start-debug
### Development Environment
upgrade-amd64: upgrade-npm upgrade-go-amd64 # Upgrades NPM & Go in local AMD64 dev environment
upgrade-arm64: upgrade-npm upgrade-go-arm64 # Upgrades NPM & Go in local ARM64 dev environment
upgrade-npm:
$(info Upgrading NPM in dev environment...)
sudo npm update -g npm
dev-go-amd64:
$(info Installing Go in local AMD64 dev environment...)
upgrade-go-amd64:
$(info Upgrading Go in AMD64 dev environment...)
sudo docker/scripts/install-go.sh amd64
go build -v ./...
acceptance-restart:
cp -f storage/acceptance/backup.db storage/acceptance/index.db
cp -f storage/acceptance/config/settingsBackup.yml storage/acceptance/config/settings.yml
rm -rf storage/acceptance/sidecar/2020
rm -rf storage/acceptance/sidecar/2011
rm -rf storage/acceptance/originals/2010
rm -rf storage/acceptance/originals/2020
rm -rf storage/acceptance/originals/2011
rm -rf storage/acceptance/originals/2013
rm -rf storage/acceptance/originals/2017
go run cmd/photoprism/photoprism.go --public --upload-nsfw=false --database-driver sqlite --database-dsn ./storage/acceptance/index.db --import-path ./storage/acceptance/import --http-port=2343 --config-path ./storage/acceptance/config --originals-path ./storage/acceptance/originals --storage-path ./storage/acceptance --test --backup-path ./storage/acceptance/backup --disable-backups start -d
acceptance-stop:
go run cmd/photoprism/photoprism.go --public --upload-nsfw=false --database-driver sqlite --database-dsn ./storage/acceptance/index.db --import-path ./storage/acceptance/import --http-port=2343 --config-path ./storage/acceptance/config --originals-path ./storage/acceptance/originals --storage-path ./storage/acceptance --test --backup-path ./storage/acceptance/backup --disable-backups stop
acceptance-private-restart:
cp -f storage/acceptance/backup.db storage/acceptance/index.db
cp -f storage/acceptance/config/settingsBackup.yml storage/acceptance/config/settings.yml
go run cmd/photoprism/photoprism.go --public=false --upload-nsfw=false --database-driver sqlite --database-dsn ./storage/acceptance/index.db --import-path ./storage/acceptance/import --http-port=2343 --config-path ./storage/acceptance/config --originals-path ./storage/acceptance/originals --storage-path ./storage/acceptance --test --backup-path ./storage/acceptance/backup --disable-backups start -d
acceptance-private-stop:
go run cmd/photoprism/photoprism.go --public=false --upload-nsfw=false --database-driver sqlite --database-dsn ./storage/acceptance/index.db --import-path ./storage/acceptance/import --http-port=2343 --config-path ./storage/acceptance/config --originals-path ./storage/acceptance/originals --storage-path ./storage/acceptance --test --backup-path ./storage/acceptance/backup --disable-backups stop
upgrade-go-arm64:
$(info Upgrading Go in ARM64 dev environment...)
sudo docker/scripts/install-go.sh arm64
go build -v ./...
start:
go run cmd/photoprism/photoprism.go start -d
stop:
@ -87,6 +77,7 @@ generate:
@if [ ${$(shell git diff --shortstat assets/locales/messages.pot):1:45} == $(POT_UNCHANGED) ]; then\
git checkout -- assets/locales/messages.pot;\
fi
### Production Build & Installation
install-bin:
scripts/build.sh prod ~/.local/bin/$(BINARY_NAME)
install-assets:
@ -99,12 +90,7 @@ install-assets:
mkdir -p ~/Pictures/Import
cp -r assets/locales assets/facenet assets/nasnet assets/nsfw assets/profiles assets/static assets/templates ~/.photoprism/assets
find ~/.photoprism/assets -name '.*' -type f -delete
clean-local-assets:
rm -rf ~/.photoprism/assets/*
clean-local-cache:
rm -rf ~/.photoprism/storage/cache/*
clean-local-config:
rm -f ~/.photoprism/storage/config/*
### Dependencies
dep-list:
go list -u -m -json all | go-mod-outdated -direct
dep-js:
@ -125,11 +111,14 @@ zip-nasnet:
(cd assets && zip -r nasnet.zip nasnet -x "*/.*" -x "*/version.txt")
zip-nsfw:
(cd assets && zip -r nsfw.zip nsfw -x "*/.*" -x "*/version.txt")
### Build Commands
build-js:
(cd frontend && env NODE_ENV=production npm run build)
build-go:
rm -f $(BINARY_NAME)
scripts/build.sh debug $(BINARY_NAME)
build-go-remote:
docker-compose exec -u root photoprism make build-go
build-race:
rm -f $(BINARY_NAME)
scripts/build.sh race $(BINARY_NAME)
@ -142,11 +131,16 @@ build-tensorflow:
build-tensorflow-arm64:
docker build -t photoprism/tensorflow:arm64 docker/tensorflow/arm64
docker run -ti photoprism/tensorflow:arm64 bash
start-debug:
docker-compose exec -u root photoprism go install github.com/go-delve/delve/cmd/dlv@latest
docker-compose exec -u root photoprism dlv --listen=:40000 --headless=true --api-version=2 --accept-multiclient exec ./photoprism start
### Frontend Tests
watch-js:
(cd frontend && env NODE_ENV=development npm run watch)
test-js:
$(info Running JS unit tests...)
(cd frontend && env NODE_ENV=development BABEL_ENV=test npm run test)
### Acceptance Tests
acceptance:
$(info Running JS acceptance tests in Chrome...)
(cd frontend && npm run acceptance && cd ..)
@ -159,6 +153,38 @@ acceptance-private:
acceptance-private-firefox:
$(info Running JS acceptance-private tests in Firefox...)
(cd frontend && npm run acceptance-private-firefox && cd ..)
acceptance-openid:
$(info Running JS acceptance-private tests in Chrome...)
(cd frontend && npm run acceptance-openid && cd ..)
acceptance-openid-firefox:
$(info Running JS acceptance-private tests in Firefox...)
(cd frontend && npm run acceptance-openid-firefox && cd ..)
acceptance-restart:
cp -f storage/acceptance/backup.db storage/acceptance/index.db
cp -f storage/acceptance/config/settingsBackup.yml storage/acceptance/config/settings.yml
rm -rf storage/acceptance/sidecar/2020
rm -rf storage/acceptance/sidecar/2011
rm -rf storage/acceptance/originals/2010
rm -rf storage/acceptance/originals/2020
rm -rf storage/acceptance/originals/2011
rm -rf storage/acceptance/originals/2013
rm -rf storage/acceptance/originals/2017
go run cmd/photoprism/photoprism.go --public --upload-nsfw=false --database-driver="sqlite" --database-dsn="./storage/acceptance/index.db" --import-path="./storage/acceptance/import" --http-port=2343 --config-path="./storage/acceptance/config" --originals-path="./storage/acceptance/originals" --storage-path="./storage/acceptance" --test --backup-path="./storage/acceptance/backup" --disable-backups start -d
acceptance-stop:
go run cmd/photoprism/photoprism.go --public --upload-nsfw=false --database-driver="sqlite" --database-dsn="./storage/acceptance/index.db" --import-path="./storage/acceptance/import" --http-port=2343 --config-path="./storage/acceptance/config" --originals-path="./storage/acceptance/originals" --storage-path="./storage/acceptance" --test --backup-path="./storage/acceptance/backup" --disable-backups stop
acceptance-private-restart:
cp -f storage/acceptance/backup.db storage/acceptance/index.db
cp -f storage/acceptance/config/settingsBackup.yml storage/acceptance/config/settings.yml
go run cmd/photoprism/photoprism.go --oidc-issuer-url="" --oidc-client-id="" --oidc-client-secret="" --public=false --upload-nsfw=false --database-driver="sqlite" --database-dsn="./storage/acceptance/index.db" --import-path="./storage/acceptance/import" --http-port=2343 --config-path="./storage/acceptance/config" --originals-path="./storage/acceptance/originals" --storage-path="./storage/acceptance" --test --backup-path="./storage/acceptance/backup" --disable-backups start -d
acceptance-private-stop:
go run cmd/photoprism/photoprism.go --oidc-issuer-url="" --oidc-client-id="" --oidc-client-secret="" --public=false --upload-nsfw=false --database-driver="sqlite" --database-dsn="./storage/acceptance/index.db" --import-path="./storage/acceptance/import" --http-port=2343 --config-path="./storage/acceptance/config" --originals-path="./storage/acceptance/originals" --storage-path="./storage/acceptance" --test --backup-path="./storage/acceptance/backup" --disable-backups stop
acceptance-openid-restart:
cp -f storage/acceptance/backup.db storage/acceptance/index.db
cp -f storage/acceptance/config/settingsBackup.yml storage/acceptance/config/settings.yml
go run cmd/photoprism/photoprism.go --public=false --upload-nsfw=false --database-driver="sqlite" --database-dsn="./storage/acceptance/index.db" --import-path="./storage/acceptance/import" --http-port=2342 --config-path="./storage/acceptance/config" --originals-path="./storage/acceptance/originals" --storage-path="./storage/acceptance" --test --backup-path="./storage/acceptance/backup" --disable-backups start -d
acceptance-openid-stop:
go run cmd/photoprism/photoprism.go --public=false --upload-nsfw=false --database-driver="sqlite" --database-dsn="./storage/acceptance/index.db" --import-path="./storage/acceptance/import" --http-port=2342 --config-path="./storage/acceptance/config" --originals-path="./storage/acceptance/originals" --storage-path="./storage/acceptance" --test --backup-path="./storage/acceptance/backup" --disable-backups stop
### Backend Tests
reset-mariadb:
$(info Resetting photoprism database...)
mysql < scripts/sql/reset-mariadb.sql
@ -194,38 +220,29 @@ test-coverage:
$(info Running all Go unit tests with code coverage report...)
go test -parallel 1 -count 1 -cpu 1 -failfast -tags slow -timeout 30m -coverprofile coverage.txt -covermode atomic ./pkg/... ./internal/...
go tool cover -html=coverage.txt -o coverage.html
clean:
rm -f $(BINARY_NAME)
rm -f *.log
rm -rf node_modules
rm -rf storage/testdata
rm -rf storage/backup
rm -rf storage/cache
rm -rf frontend/node_modules
docker-development:
### 64-bit Multi-Arch Docker Image
docker-develop:
docker pull --platform=amd64 ubuntu:21.10
docker pull --platform=arm64 ubuntu:21.10
scripts/docker/multiarch.sh development linux/amd64,linux/arm64 $(DOCKER_TAG)
scripts/docker/multiarch.sh develop linux/amd64,linux/arm64 $(DOCKER_TAG)
docker-preview:
scripts/docker/multiarch.sh photoprism linux/amd64,linux/arm64
docker-release:
scripts/docker/multiarch.sh photoprism linux/amd64,linux/arm64 $(DOCKER_TAG)
docker-armv7-development:
docker pull --platform=arm ubuntu:21.10
scripts/docker/arch.sh development linux/arm armv7 /armv7
docker-armv7-preview:
docker pull --platform=arm photoprism/development:armv7
### ARMv7 32-bit Docker Image
armv7-develop:
scripts/docker/arch.sh develop linux/arm armv7 /armv7
armv7-preview:
docker pull --platform=arm photoprism/develop:armv7
scripts/docker/arch.sh photoprism linux/arm armv7-preview /armv7
docker-armv7-release:
docker pull --platform=arm photoprism/development:armv7
armv7-release:
docker pull --platform=arm photoprism/develop:armv7
scripts/docker/arch.sh photoprism linux/arm armv7 /armv7
### Additional Docker Images / Commands for Development & Testing
docker-local:
scripts/docker/build.sh photoprism
docker-pull:
docker pull photoprism/photoprism:preview photoprism/photoprism:latest
docker-goproxy:
docker pull golang:alpine
scripts/docker/multiarch.sh goproxy linux/amd64,linux/arm64 $(DOCKER_TAG)
docker-demo:
scripts/docker/build.sh demo $(DOCKER_TAG)
scripts/docker/push.sh demo $(DOCKER_TAG)
@ -234,20 +251,20 @@ docker-demo-local:
scripts/docker/build.sh demo $(DOCKER_TAG)
scripts/docker/push.sh demo $(DOCKER_TAG)
docker-dummy-webdav:
docker pull --platform=amd64 golang:1
docker pull --platform=arm64 golang:1
docker pull golang:alpine
scripts/docker/multiarch.sh dummy-webdav linux/amd64,linux/arm64 $(DOCKER_TAG)
docker-dummy-oidc:
docker pull --platform=amd64 golang:1
docker pull --platform=arm64 golang:1
docker pull golang:alpine
scripts/docker/multiarch.sh dummy-oidc linux/amd64,linux/arm64 $(DOCKER_TAG)
packer-digitalocean:
$(info Buildinng DigitalOcean marketplace image...)
(cd ./docker/examples/cloud && packer build digitalocean.json)
### CI
drone-sign:
drone sign photoprism/photoprism --save
lint-js:
(cd frontend && npm run lint)
### Build & Code Clean-Up
fmt-js:
(cd frontend && npm run fmt)
fmt-go:
@ -255,3 +272,17 @@ fmt-go:
goimports -w pkg internal cmd
tidy:
go mod tidy
clean:
rm -f $(BINARY_NAME)
rm -f *.log
rm -rf node_modules
rm -rf storage/testdata
rm -rf storage/backup
rm -rf storage/cache
rm -rf frontend/node_modules
clean-local-assets:
rm -rf ~/.photoprism/assets/*
clean-local-cache:
rm -rf ~/.photoprism/storage/cache/*
clean-local-config:
rm -f ~/.photoprism/storage/config/*

View file

@ -0,0 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0{{if not .config.Settings.UI.Zoom }}, maximum-scale=1.0, user-scalable=no{{end}}">
<title>{{ .config.SiteTitle }}</title>
<meta property="og:url" content="{{ .config.SiteUrl }}">
<meta property="og:type" content="website">
<meta property="og:title" content="{{ .config.SiteTitle }}{{if .config.SiteCaption}}: {{ .config.SiteCaption }}{{end}}">
<meta property="og:image" content="{{ .config.SitePreview }}">
<meta property="og:description" content="{{ .config.SiteDescription }}">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ .config.SiteTitle }}{{if .config.SiteCaption}}: {{ .config.SiteCaption }}{{end}}">
<meta name="twitter:image" content="{{ .config.SitePreview }}">
<meta name="twitter:description" content="{{ .config.SiteDescription }}">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="{{ .config.SiteTitle }}">
{{if .config.SiteAuthor}}<meta name="author" content="{{ .config.SiteAuthor }}">{{end}}
{{if .config.SiteDescription}}<meta name="description" content="{{ .config.SiteDescription }}"/>{{end}}
{{template "favicons.tmpl" .}}
<link rel="stylesheet" href="/static/build/app.css?{{ .config.CSSHash }}">
<link rel="manifest" href="/static/manifest.json?{{ .config.ManifestHash }}">
<script>
window.__CONFIG__ = {{ .config }};
</script>
</head>
<body class="{{ .config.Flags }}">
{{ if and .status (eq .status "ok") }}
<p class="browserupgrade">Login successful. You can safely close this tab.</p>
{{ else if and .status (eq .status "error") }}
<p class="browserupgrade">Login Error: {{ .errors }}</p>
{{ else }}
<p class="browserupgrade">Undefined State or deprecated link user flow...</p>
{{ end }}
<script>
{{ if and .status (eq .status "ok") }}
window.localStorage.setItem("session_id", {{ .id }})
window.localStorage.setItem("data", JSON.stringify({{ .data }}));
window.localStorage.setItem("config", JSON.stringify({{ .config }}));
window.sessionStorage.removeItem("preventAutoLogin");
window.location.href = '/login';
{{ else if and .status (eq .status "error") }}
window.localStorage.setItem("auth_error", {{ .errors }});
window.location.href = '/login?preventAutoLogin=true';
{{ end }}
</script>
</body>
</html>

View file

@ -39,7 +39,7 @@ import (
"github.com/urfave/cli"
)
var version = "development"
var version = "develop"
var log = event.Log
func main() {

View file

@ -17,11 +17,11 @@ services:
- "~/.cache/npm:/root/.cache/npm"
- "~/.cache/go-mod:/go/pkg/mod"
environment:
GOPROXY: ${GOPROXY:-http://goproxy:8888,direct}
GOPROXY: "https://proxy.golang.org,direct"
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
PHOTOPRISM_SITE_DESCRIPTION: "AI-Powered Photos App. Tags and finds pictures without getting in your way!"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"
PHOTOPRISM_DEBUG: "false"
PHOTOPRISM_READONLY: "false"
@ -152,7 +152,7 @@ services:
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
@ -176,7 +176,11 @@ services:
## Dummy WebDAV Server
dummy-webdav:
image: photoprism/dummy-webdav:20211109
image: photoprism/dummy-webdav:20211218
environment:
WEBDAV_USERNAME: admin
WEBDAV_PASSWORD: photoprism
## Dummy OpenID Connect Server
dummy-oidc:
image: photoprism/dummy-oidc:20211218

View file

@ -1,9 +1,9 @@
version: '3.5'
# Stable Release (for developers only)
## Latest Stable Release for QA
services:
## App Server (required)
## App Server
## Docs: https://docs.photoprism.org/
photoprism-latest:
image: photoprism/photoprism:latest
security_opt:
@ -11,13 +11,23 @@ services:
- apparmor:unconfined
ports:
- "2344:2342" # HTTP port (host:container)
labels:
- "traefik.enable=true"
- "traefik.http.services.photoprism-latest.loadbalancer.server.port=2342"
- "traefik.http.routers.photoprism-latest.entrypoints=websecure"
- "traefik.http.routers.photoprism-latest.rule=Host(`photoprism-latest.traefik.net`)"
- "traefik.http.routers.photoprism-latest.tls.domains[0].main=traefik.net"
- "traefik.http.routers.photoprism-latest.tls.domains[0].sans=*.traefik.net"
- "traefik.http.routers.photoprism-latest.tls=true"
environment:
PHOTOPRISM_UID: ${UID:-1000}
PHOTOPRISM_GID: ${GID:-1000}
PHOTOPRISM_SITE_URL: "http://localhost:2344/"
PHOTOPRISM_UID: ${UID:-1000} # User ID
PHOTOPRISM_GID: ${GID:-1000} # Group ID
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # Admin password (min 4 characters)
## Public server URL incl http:// or https:// and /path, :port is optional
PHOTOPRISM_SITE_URL: "https://photoprism-latest.traefik.net/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
PHOTOPRISM_SITE_DESCRIPTION: "AI-Powered Photos App. Tags and finds pictures without getting in your way!"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"
PHOTOPRISM_DEBUG: "true"
PHOTOPRISM_READONLY: "false"
@ -29,10 +39,9 @@ services:
PHOTOPRISM_HTTP_COMPRESSION: "gzip" # Improves transfer speed and bandwidth utilization (none or gzip)
PHOTOPRISM_DATABASE_DRIVER: "mysql"
PHOTOPRISM_DATABASE_SERVER: "mariadb:4001"
PHOTOPRISM_DATABASE_NAME: "latest"
PHOTOPRISM_DATABASE_USER: "root"
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # The initial admin password (min 4 characters)
PHOTOPRISM_DATABASE_NAME: "photoprism_latest"
PHOTOPRISM_DATABASE_USER: "photoprism_latest"
PHOTOPRISM_DATABASE_PASSWORD: "photoprism_latest"
PHOTOPRISM_DISABLE_CHOWN: "false" # Disables storage permission updates on startup
PHOTOPRISM_DISABLE_BACKUPS: "false" # Don't backup photo and album metadata to YAML files
PHOTOPRISM_DISABLE_WEBDAV: "false" # Disables built-in WebDAV server
@ -56,7 +65,8 @@ services:
- "./storage/latest:/photoprism/storage"
- "./storage/originals:/photoprism/originals"
## Join shared "photoprism-develop" network
networks:
default:
external:
name: shared
name: photoprism-develop

View file

@ -1,9 +1,40 @@
version: '3.5'
# Legacy Databases Servers (for developers only)
## MariaDB Server Versions for Development & Testing
services:
## Affected by MDEV-25362: Incorrect name resolution for subqueries in ON expressions
## MariaDB 10.7 Database Server
mariadb-10-7:
image: mariadb:10.7
command: mysqld --port=4001 --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
## MariaDB 10.6 Database Server
## Docs: https://mariadb.com/docs/reference/cs10.6/
mariadb:
image: mariadb:10.6
command: mysqld --port=4001 --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001"
ports:
- "4001:4001" # Database port (host:container)
volumes:
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
## MariaDB 10.5.5 Database Server
## affected by MDEV-25362: Incorrect name resolution for subqueries in ON expressions
## see https://jira.mariadb.org/browse/MDEV-25362
mariadb-10-5-5:
image: mariadb:10.5.5
@ -11,53 +42,59 @@ services:
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
## MariaDB 10.3 Database Server
## Docs: https://mariadb.com/docs/reference/cs10.3/
mariadb-10-3:
image: mariadb:10.3
command: mysqld --port=4001 --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
## MariaDB 10.2 Database Server
## Docs: https://mariadb.com/docs/reference/cs10.2/
mariadb-10-2:
image: mariadb:10.2
command: mysqld --port=4001 --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
## MariaDB 10.1 Database Server
mariadb-10-1:
image: mariadb:10.1
command: mysqld --port=4001 --transaction-isolation=READ-COMMITTED --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --max-connections=512 --innodb-rollback-on-timeout=OFF --innodb-lock-wait-timeout=120
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
## Join shared "photoprism-develop" network
networks:
default:
external:
name: shared
name: photoprism-develop

View file

@ -29,10 +29,14 @@ services:
- "go-mod:/go/pkg/mod"
shm_size: "2gb"
environment:
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
GOPROXY: "https://proxy.golang.org,direct"
PHOTOPRISM_UID: ${UID:-1000} # User ID
PHOTOPRISM_GID: ${GID:-1000} # Group ID
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # Admin password (min 4 characters)
PHOTOPRISM_SITE_URL: "http://localhost:2342/" # Public server URL incl http:// or https:// and /path, :port is option
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
PHOTOPRISM_SITE_DESCRIPTION: "AI-Powered Photos App. Tags and finds pictures without getting in your way!"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"
PHOTOPRISM_DEBUG: "true"
PHOTOPRISM_READONLY: "false"
@ -49,7 +53,6 @@ services:
PHOTOPRISM_DATABASE_PASSWORD: "photoprism"
PHOTOPRISM_TEST_DRIVER: "sqlite"
PHOTOPRISM_TEST_DSN: ".test.db"
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # The initial admin password (min 4 characters)
PHOTOPRISM_ASSETS_PATH: "/go/src/github.com/photoprism/photoprism/assets"
PHOTOPRISM_STORAGE_PATH: "/go/src/github.com/photoprism/photoprism/storage"
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
@ -85,11 +88,18 @@ services:
## Dummy WebDAV Server
dummy-webdav:
image: photoprism/dummy-webdav:20211109
image: photoprism/dummy-webdav:20211218
environment:
WEBDAV_USERNAME: admin
WEBDAV_PASSWORD: photoprism
## Dummy OpenID Connect Server
dummy-oidc:
image: photoprism/dummy-oidc:20211218
# Expose port 9998 on host
# ports:
# - "9998:9998"
volumes:
go-mod:
driver: local

View file

@ -7,7 +7,19 @@ version: '3.5'
## - Keycloak OpenID Connect Provider
## - and Dummy Services
services:
## App Server
## Traefik v2.5.5 HTTPS Reverse Proxy
## Includes test certificates for https://*.traefik.net/
## Docs: https://doc.traefik.io/traefik/
traefik:
image: photoprism/traefik:20211218
ports:
# - "80:80" # HTTP (redirects to HTTPS)
- "443:443" # HTTPS (required)
volumes:
- "/var/run/docker.sock:/var/run/docker.sock" # Host names are configured with Docker labels
## App Build Environment
## Docs: https://docs.photoprism.org/developer-guide/
photoprism:
build: .
image: photoprism/photoprism:develop
@ -18,18 +30,33 @@ services:
- seccomp:unconfined
- apparmor:unconfined
ports:
- "2342:2342" # Default HTTP port (host:container)
- "2343:2343" # Acceptance Test HTTP port (host:container)
- "2342:2342" # Default HTTP port (host:container)
- "2343:2343" # Acceptance Test HTTP port (host:container)
- "40000:40000" # Go Debugger (host:container)
shm_size: "2gb"
links:
- "traefik:keycloak.traefik.net"
- "traefik:photoprism.traefik.net"
- "traefik:dummy-webdav.traefik.net"
- "traefik:dummy-oidc.traefik.net"
labels:
- "traefik.enable=true"
- "traefik.http.services.photoprism.loadbalancer.server.port=2342"
- "traefik.http.routers.photoprism.entrypoints=websecure"
- "traefik.http.routers.photoprism.rule=Host(`photoprism.traefik.net`)"
- "traefik.http.routers.photoprism.tls.domains[0].main=traefik.net"
- "traefik.http.routers.photoprism.tls.domains[0].sans=*.traefik.net"
- "traefik.http.routers.photoprism.tls=true"
environment:
GOPROXY: "https://proxy.golang.org,direct"
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # The initial admin password (min 4 characters)
PHOTOPRISM_UID: ${UID:-1000}
PHOTOPRISM_GID: ${GID:-1000}
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
PHOTOPRISM_UID: ${UID:-1000} # User ID
PHOTOPRISM_GID: ${GID:-1000} # Group ID
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # Admin password (min 4 characters)
## External development server URL incl http:// or https:// and /path, :port is optional
PHOTOPRISM_SITE_URL: "https://photoprism.traefik.net/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "Browse Your Life"
PHOTOPRISM_SITE_DESCRIPTION: "AI-powered app for browsing, organizing & sharing your photo collection."
PHOTOPRISM_SITE_DESCRIPTION: "AI-Powered Photos App. Tags and finds pictures without getting in your way!"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"
PHOTOPRISM_DEBUG: "true"
PHOTOPRISM_READONLY: "false"
@ -69,6 +96,10 @@ services:
PHOTOPRISM_JPEG_SIZE: 7680 # Size limit for converted image files in pixels (720-30000)
PHOTOPRISM_JPEG_QUALITY: 92 # Set to 95 for high-quality thumbnails (25-100)
TF_CPP_MIN_LOG_LEVEL: 0 # Show TensorFlow log messages for development
## OpenID Connect Provider (pre-configured for local Keycloak test server):
PHOTOPRISM_OIDC_ISSUER_URL: "https://keycloak.traefik.net/auth/realms/master"
PHOTOPRISM_OIDC_CLIENT_ID: "photoprism-develop"
PHOTOPRISM_OIDC_CLIENT_SECRET: "9d8351a0-ca01-4556-9c37-85eb634869b9"
## Enable TensorFlow AVX2 support for modern Intel CPUs (requires starting the container as root):
# PHOTOPRISM_INIT: "tensorflow-amd64-avx2"
## Hardware video transcoding config (optional):
@ -98,7 +129,7 @@ services:
ports:
- "4001:4001" # Database port (host:container)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
@ -113,25 +144,73 @@ services:
expose:
- "4001" # Database port (internal)
volumes:
- "./scripts/sql/init-test-databases.sql:/docker-entrypoint-initdb.d/init-test-databases.sql"
- "./scripts/sql/mariadb-init.sql:/docker-entrypoint-initdb.d/init.sql"
environment:
MYSQL_ROOT_PASSWORD: photoprism
MYSQL_USER: photoprism
MYSQL_PASSWORD: photoprism
MYSQL_DATABASE: photoprism
## Keycloak OpenID Connect Provider
## Test User: user / photoprism
## Test Admin: admin / photoprism
## Docs: https://www.keycloak.org/getting-started/getting-started-docker
keycloak:
image: quay.io/keycloak/keycloak:16.0.0
links:
- "traefik:photoprism.traefik.net"
labels:
- "traefik.enable=true"
- "traefik.http.services.keycloak.loadbalancer.server.port=8080"
- "traefik.http.routers.keycloak.entrypoints=websecure"
- "traefik.http.routers.keycloak.rule=Host(`keycloak.traefik.net`)"
- "traefik.http.routers.keycloak.tls.domains[0].main=traefik.net"
- "traefik.http.routers.keycloak.tls.domains[0].sans=*.traefik.net"
- "traefik.http.routers.keycloak.tls=true"
environment:
KEYCLOAK_USER: "admin"
KEYCLOAK_PASSWORD: "photoprism"
KEYCLOAK_FRONTEND_URL: "https://keycloak.traefik.net/auth"
DB_VENDOR: "mariadb"
DB_PORT: 4001
DB_DATABASE: "keycloak"
DB_USER: "keycloak"
DB_PASSWORD: "keycloak"
## Dummy WebDAV Server
dummy-webdav:
image: photoprism/dummy-webdav:20211109
image: photoprism/dummy-webdav:20211218
environment:
WEBDAV_USERNAME: admin
WEBDAV_PASSWORD: photoprism
labels:
- "traefik.enable=true"
- "traefik.http.services.dummy-webdav.loadbalancer.server.port=80"
- "traefik.http.routers.dummy-webdav.entrypoints=websecure"
- "traefik.http.routers.dummy-webdav.rule=Host(`dummy-webdav.traefik.net`)"
- "traefik.http.routers.dummy-webdav.tls.domains[0].main=traefik.net"
- "traefik.http.routers.dummy-webdav.tls.domains[0].sans=*.traefik.net"
- "traefik.http.routers.dummy-webdav.tls=true"
## Dummy OpenID Connect Server
dummy-oidc:
image: photoprism/dummy-oidc:20211218
labels:
- "traefik.enable=true"
- "traefik.http.services.dummy-oidc.loadbalancer.server.port=9998"
- "traefik.http.routers.dummy-oidc.entrypoints=websecure"
- "traefik.http.routers.dummy-oidc.rule=Host(`dummy-oidc.traefik.net`)"
- "traefik.http.routers.dummy-oidc.tls.domains[0].main=traefik.net"
- "traefik.http.routers.dummy-oidc.tls.domains[0].sans=*.traefik.net"
- "traefik.http.routers.dummy-oidc.tls=true"
## Create named volume for Go module cache
volumes:
go-mod:
driver: local
## Create shared "photoprism-develop" network for connecting with services in other docker-compose.yml files
networks:
default:
name: shared
name: photoprism-develop
driver: bridge

View file

@ -101,7 +101,7 @@ RUN rm -rf /tmp/* && mkdir -p /tmp/photoprism && \
# Copy additional scripts to image
COPY --chown=root:root /docker/scripts/heif-convert.sh /usr/local/bin/heif-convert
COPY --chown=root:root /docker/scripts/Makefile /root/Makefile
COPY --chown=root:root /docker/development/entrypoint.sh /entrypoint.sh
COPY --chown=root:root /docker/develop/entrypoint.sh /entrypoint.sh
# Install Go tools
RUN /usr/local/go/bin/go install github.com/tianon/gosu@latest && \
@ -124,8 +124,8 @@ RUN useradd -m -U -u 1000 -d /photoprism photoprism && chmod a+rwx /photoprism &
find /go -type d -print0 | xargs -0 chmod 777
# Copy mysql client config for development
COPY --chown=root:root /docker/development/.my.cnf /root/.my.cnf
COPY --chown=photoprism:photoprism /docker/development/.my.cnf /photoprism/.my.cnf
COPY --chown=root:root /docker/develop/.my.cnf /root/.my.cnf
COPY --chown=photoprism:photoprism /docker/develop/.my.cnf /photoprism/.my.cnf
RUN chmod 644 /root/.my.cnf /photoprism/.my.cnf
# Set up project directory

View file

@ -93,7 +93,7 @@ RUN rm -rf /tmp/* && mkdir -p /tmp/photoprism && \
# Copy additional scripts to image
COPY --chown=root:root /docker/scripts/heif-convert.sh /usr/local/bin/heif-convert
COPY --chown=root:root /docker/scripts/Makefile /root/Makefile
COPY --chown=root:root /docker/development/entrypoint.sh /entrypoint.sh
COPY --chown=root:root /docker/develop/entrypoint.sh /entrypoint.sh
# Install Go tools
RUN /usr/local/go/bin/go install github.com/tianon/gosu@latest && \

View file

@ -0,0 +1,71 @@
{
"clientId": "photoprism-develop",
"secret": "9d8351a0-ca01-4556-9c37-85eb634869b9",
"name": "PhotoPrism",
"rootUrl": "https://photoprism.traefik.net/",
"adminUrl": "https://photoprism.traefik.net/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"https://photoprism.traefik.net/api/v1/auth/callback"
],
"webOrigins": [
"https://photoprism.traefik.net"
],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": false,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"id.token.as.detached.signature": "false",
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"use.refresh.tokens": "true",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"saml.artifact.binding": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"require.pushed.authorization.requests": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}

View file

@ -0,0 +1,75 @@
{
"clients": [
{
"clientId": "photoprism-develop",
"secret": "9d8351a0-ca01-4556-9c37-85eb634869b9",
"name": "PhotoPrism",
"rootUrl": "https://photoprism.traefik.net/",
"adminUrl": "https://photoprism.traefik.net/",
"surrogateAuthRequired": false,
"enabled": true,
"alwaysDisplayInConsole": false,
"clientAuthenticatorType": "client-secret",
"redirectUris": [
"https://photoprism.traefik.net/api/v1/auth/callback"
],
"webOrigins": [
"https://photoprism.traefik.net"
],
"notBefore": 0,
"bearerOnly": false,
"consentRequired": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": true,
"serviceAccountsEnabled": false,
"publicClient": false,
"frontchannelLogout": false,
"protocol": "openid-connect",
"attributes": {
"id.token.as.detached.signature": "false",
"saml.assertion.signature": "false",
"saml.force.post.binding": "false",
"saml.multivalued.roles": "false",
"saml.encrypt": "false",
"oauth2.device.authorization.grant.enabled": "false",
"backchannel.logout.revoke.offline.tokens": "false",
"saml.server.signature": "false",
"saml.server.signature.keyinfo.ext": "false",
"use.refresh.tokens": "true",
"exclude.session.state.from.auth.response": "false",
"oidc.ciba.grant.enabled": "false",
"saml.artifact.binding": "false",
"backchannel.logout.session.required": "true",
"client_credentials.use_refresh_token": "false",
"saml_force_name_id_format": "false",
"require.pushed.authorization.requests": "false",
"saml.client.signature": "false",
"tls.client.certificate.bound.access.tokens": "false",
"saml.authnstatement": "false",
"display.on.consent.screen": "false",
"saml.onetimeuse.condition": "false"
},
"authenticationFlowBindingOverrides": {},
"fullScopeAllowed": true,
"nodeReRegistrationTimeout": -1,
"defaultClientScopes": [
"web-origins",
"profile",
"roles",
"email"
],
"optionalClientScopes": [
"address",
"phone",
"offline_access",
"microprofile-jwt"
],
"access": {
"view": true,
"configure": true,
"manage": true
}
}
]
}

View file

@ -1,11 +1,14 @@
FROM golang:1
##################################################### BUILD STAGE ######################################################
FROM golang:alpine AS build
# Move to working directory /app
WORKDIR /app
LABEL maintainer="Michael Mayer <hello@photoprism.app>"
ARG GOPROXY
ARG GODEBUG
# Move to working directory /app
WORKDIR /app
# Copy files and download dependency using go mod
COPY /docker/dummy/oidc/app/. .
RUN go mod download

View file

@ -3,7 +3,7 @@ module caos-test-op
go 1.17
require (
github.com/caos/oidc v0.15.10
github.com/caos/oidc v1.0.0
github.com/gorilla/mux v1.8.0
gopkg.in/square/go-jose.v2 v2.6.0
)

View file

@ -37,6 +37,8 @@ github.com/caos/logging v0.0.2 h1:ebg5C/HN0ludYR+WkvnFjwSExF4wvyiWPyWGcKMYsoo=
github.com/caos/logging v0.0.2/go.mod h1:9LKiDE2ChuGv6CHYif/kiugrfEXu9AwDiFWSreX7Wp0=
github.com/caos/oidc v0.15.10 h1:dSzkIvsZR2PSZgvBFFkLJt8A/MujsyLac1yNvBShXuw=
github.com/caos/oidc v0.15.10/go.mod h1:4l0PPwdc6BbrdCFhNrRTUddsG292uHGa7gE2DSEIqoU=
github.com/caos/oidc v1.0.0 h1:3sHkYf8zsuARR89qO9CyvfYhHGdliWPcou4glzGMXmQ=
github.com/caos/oidc v1.0.0/go.mod h1:4l0PPwdc6BbrdCFhNrRTUddsG292uHGa7gE2DSEIqoU=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=

View file

@ -37,10 +37,16 @@ func NewAuthStorage() op.Storage {
type AuthRequest struct {
ID string
ResponseType oidc.ResponseType
ResponseMode oidc.ResponseMode
RedirectURI string
Nonce string
ClientID string
CodeChallenge *oidc.CodeChallenge
State string
}
func (a *AuthRequest) GetResponseMode() oidc.ResponseMode {
return a.ResponseMode
}
func (a *AuthRequest) GetACR() string {
@ -102,7 +108,7 @@ func (a *AuthRequest) GetScopes() []string {
func (a *AuthRequest) SetCurrentScopes(scopes []string) {}
func (a *AuthRequest) GetState() string {
return state
return a.State
}
func (a *AuthRequest) GetSubject() string {
@ -114,10 +120,9 @@ func (a *AuthRequest) Done() bool {
}
var (
a = &AuthRequest{}
t bool
c string
state string
a = &AuthRequest{}
t bool
c string
)
func (s *AuthStorage) Health(ctx context.Context) error {
@ -126,7 +131,6 @@ func (s *AuthStorage) Health(ctx context.Context) error {
func (s *AuthStorage) CreateAuthRequest(_ context.Context, authReq *oidc.AuthRequest, userId string) (op.AuthRequest, error) {
fmt.Println("Userid: ", userId)
fmt.Println("CreateAuthRequest ID: ", authReq.ID)
fmt.Println("CreateAuthRequest CodeChallenge: ", authReq.CodeChallenge)
fmt.Println("CreateAuthRequest CodeChallengeMethod: ", authReq.CodeChallengeMethod)
fmt.Println("CreateAuthRequest State: ", authReq.State)
@ -137,14 +141,13 @@ func (s *AuthStorage) CreateAuthRequest(_ context.Context, authReq *oidc.AuthReq
fmt.Println("CreateAuthRequest Display: ", authReq.Display)
fmt.Println("CreateAuthRequest LoginHint: ", authReq.LoginHint)
fmt.Println("CreateAuthRequest IDTokenHint: ", authReq.IDTokenHint)
a = &AuthRequest{ID: "authReqUserAgentId", ClientID: authReq.ClientID, ResponseType: authReq.ResponseType, Nonce: authReq.Nonce, RedirectURI: authReq.RedirectURI}
a = &AuthRequest{ID: "authReqUserAgentId", ClientID: authReq.ClientID, ResponseType: authReq.ResponseType, Nonce: authReq.Nonce, RedirectURI: authReq.RedirectURI, State: authReq.State}
if authReq.CodeChallenge != "" {
a.CodeChallenge = &oidc.CodeChallenge{
Challenge: authReq.CodeChallenge,
Method: authReq.CodeChallengeMethod,
}
}
state = authReq.State
t = false
return a, nil
}
@ -294,6 +297,10 @@ func (s *AuthStorage) ValidateJWTProfileScopes(ctx context.Context, userID strin
return scope, nil
}
func (s *AuthStorage) RevokeToken(ctx context.Context, token string, userID string, clientID string) *oidc.Error {
return nil
}
type ConfClient struct {
applicationType op.ApplicationType
authMethod oidc.AuthMethod

View file

@ -6,47 +6,64 @@ import (
"crypto/sha256"
"log"
"net/http"
"github.com/gorilla/mux"
"os"
"caos-test-op/mock"
"github.com/caos/oidc/pkg/op"
"github.com/gorilla/mux"
)
func main() {
ctx := context.Background()
var port, issuer string
if port = os.Getenv("DUMMY_OIDC_PORT"); port == "" {
port = "9998"
}
if issuer = os.Getenv("DUMMY_OIDC_ISSUER"); issuer == "" {
issuer = "http://dummy-oidc:9998"
}
b := make([]byte, 32)
rand.Read(b)
cryptoKey := sha256.Sum256(b)
port := "9998"
config := &op.Config{
Issuer: "http://dummy-oidc:9998",
CryptoKey: sha256.Sum256(b),
Issuer: issuer,
CryptoKey: cryptoKey,
CodeMethodS256: true,
}
storage := mock.NewAuthStorage()
handler, err := op.NewOpenIDProvider(ctx, config, storage)
if err != nil {
log.Fatal(err)
}
router := handler.HttpHandler().(*mux.Router)
router.Methods("GET").Path("/login").HandlerFunc(HandleLogin)
server := &http.Server{
Addr: ":" + port,
Handler: router,
}
err = server.ListenAndServe()
if err != nil {
log.Fatal(err)
}
<-ctx.Done()
}
func HandleLogin(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
requestId := r.Form.Get("id")
// simulate user login and retrieve a token that indicates a successfully logged-in user
usertoken := requestId + ":usertoken"

View file

@ -1,4 +1,5 @@
FROM golang:1
##################################################### BUILD STAGE ######################################################
FROM golang:alpine AS build
LABEL maintainer="Michael Mayer <hello@photoprism.app>"
@ -19,4 +20,4 @@ EXPOSE 80
COPY /docker/dummy/webdav/config.yml /webdav/config.yml
COPY /docker/dummy/webdav/files /webdav/files
CMD webdav -c /webdav/config.yml
CMD ["/go/bin/webdav", "-c", "/webdav/config.yml"]

View file

@ -6,8 +6,8 @@ SOFTWARE INCLUDED
PhotoPrism latest, AGPL 3
Docker CE 20.10, Apache 2
Traefik 2.4, MIT
MariaDB 10.5, GPL 2
Traefik 2.5, MIT
MariaDB 10.6, GPL 2
Ofelia 0.3.4, MIT
Watchtower 1.3, Apache 2

View file

@ -15,8 +15,8 @@ SOFTWARE INCLUDED
- [PhotoPrism latest](https://docs.photoprism.app/release-notes/), AGPL 3
- [Docker CE 20.10](https://docs.docker.com/engine/release-notes/), Apache 2
- [Traefik 2.4](https://github.com/traefik/traefik/releases), MIT
- [MariaDB 10.5](https://mariadb.com/kb/en/release-notes/), GPL 2
- [Traefik 2.5](https://github.com/traefik/traefik/releases), MIT
- [MariaDB 10.6](https://mariadb.com/kb/en/release-notes/), GPL 2
- [Ofelia 0.3.4](https://github.com/mcuadros/ofelia/releases), MIT
- [Watchtower 1.3](https://github.com/containrrr/watchtower/releases), Apache 2

View file

@ -171,7 +171,7 @@ services:
## see https://docs.photoprism.app/getting-started/proxies/traefik/
traefik:
restart: always
image: traefik:v2.4
image: traefik:v2.5
container_name: traefik
ports:
- "80:80"

View file

@ -1,29 +0,0 @@
##################################################### BUILD STAGE ######################################################
FROM golang:alpine AS build
ARG GOPROXY
ARG GODEBUG
RUN apk add --no-cache -U make git mercurial subversion
RUN git clone https://github.com/goproxyio/goproxy.git /src/goproxy && \
cd /src/goproxy && \
export CGO_ENABLED=0 && \
make
################################################## PRODUCTION STAGE ####################################################
FROM golang:alpine
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static-amd64 /usr/bin/tini
RUN chmod +x /usr/bin/tini
RUN apk add --no-cache -U git mercurial subversion
COPY --from=build /src/goproxy/bin/goproxy /goproxy
VOLUME "/go"
EXPOSE 8888
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/goproxy", "-listen", "0.0.0.0:8888"]

View file

@ -1,5 +1,5 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/development:20211210 as build
FROM photoprism/develop:20211218 as build
ARG TARGETARCH
ARG TARGETPLATFORM

View file

@ -1,5 +1,5 @@
##################################################### BUILD STAGE ######################################################
FROM photoprism/development:armv7 as build
FROM photoprism/develop:armv7 as build
ARG TARGETARCH
ARG TARGETPLATFORM

View file

@ -10,10 +10,10 @@ if [[ -z $1 ]]; then
else
set -eux;
if [[ $1 == "amd64" ]]; then
URL="https://go.dev/dl/go${GOLANG_VERSION}.linux-$1.tar.gz"
URL="https://go.dev/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz"
CHECKSUM="bd78114b0d441b029c8fe0341f4910370925a4d270a6a590668840675b0c653e *go.tgz"
elif [[ $1 == "arm64" ]]; then
URL="https://go.dev/dl/go{GOLANG_VERSION}.linux-$1.tar.gz"
URL="https://go.dev/dl/go${GOLANG_VERSION}.linux-arm64.tar.gz"
CHECKSUM="6f95ce3da40d9ce1355e48f31f4eb6508382415ca4d7413b1e7a3314e6430e7e *go.tgz"
elif [[ $1 == "arm" ]]; then
URL="https://go.dev/dl/go${GOLANG_VERSION}.linux-armv6l.tar.gz"

View file

@ -1885,9 +1885,9 @@
}
},
"node_modules/@types/eslint-scope": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz",
"integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==",
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.2.tgz",
"integrity": "sha512-TzgYCWoPiTeRg6RQYgtuW7iODtVoKu3RVL72k3WohqhjfaOLK5Mg2T4Tg1o2bSfu0vPkoI48wdQFv5b/Xe04wQ==",
"dependencies": {
"@types/eslint": "*",
"@types/estree": "*"
@ -3051,9 +3051,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001287",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001287.tgz",
"integrity": "sha512-4udbs9bc0hfNrcje++AxBuc6PfLNHwh3PO9kbwnfCQWyqtlzg3py0YgFu8jyRTTo85VAz4U+VLxSlID09vNtWA==",
"version": "1.0.30001289",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001289.tgz",
"integrity": "sha512-hV6x4IfrYViN8cJbGFVbjD7KCrhS/O7wfDgvevYRanJ/IN+hhxpTcXXqaxy3CzPNFe5rlqdimdEB/k7H0YzxHg==",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
@ -3233,9 +3233,9 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"node_modules/colord": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.1.tgz",
"integrity": "sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw=="
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
"integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ=="
},
"node_modules/colorette": {
"version": "2.0.16",
@ -3400,16 +3400,35 @@
}
},
"node_modules/content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"dependencies": {
"safe-buffer": "5.1.2"
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/content-disposition/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/content-security-policy-builder": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/content-security-policy-builder/-/content-security-policy-builder-2.1.0.tgz",
@ -4235,9 +4254,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.20",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.20.tgz",
"integrity": "sha512-N7ZVNrdzX8NE90OXEFBMsBf3fp8P/vVDUER3WCUZjzC7OkNTXHVoF6W9qVhq8+dA8tGnbDajzUpj2ISNVVyj+Q=="
"version": "1.4.24",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.24.tgz",
"integrity": "sha512-erwx5r69B/WFfFuF2jcNN0817BfDBdC4765kQ6WltOMuwsimlQo3JTEq0Cle+wpHralwdeX3OfAtw/mHxPK0Wg=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@ -5428,16 +5447,16 @@
}
},
"node_modules/express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz",
"integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==",
"dependencies": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"body-parser": "1.19.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie": "0.4.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
@ -5451,13 +5470,13 @@
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"proxy-addr": "~2.0.7",
"qs": "6.9.6",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"safe-buffer": "5.2.1",
"send": "0.17.2",
"serve-static": "1.14.2",
"setprototypeof": "1.2.0",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
@ -5533,42 +5552,6 @@
}
]
},
"node_modules/express/node_modules/body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"dependencies": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express/node_modules/bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express/node_modules/cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express/node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -5577,26 +5560,6 @@
"ms": "2.0.0"
}
},
"node_modules/express/node_modules/http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/express/node_modules/inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"node_modules/express/node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -5607,40 +5570,24 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"node_modules/express/node_modules/qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==",
"engines": {
"node": ">=0.6"
}
},
"node_modules/express/node_modules/raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"dependencies": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/express/node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"node_modules/express/node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"engines": {
"node": ">=0.6"
}
"node_modules/express/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/ext": {
"version": "1.6.0",
@ -5919,19 +5866,19 @@
"integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw=="
},
"node_modules/flow-parser": {
"version": "0.167.1",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.167.1.tgz",
"integrity": "sha512-KQGKp9VvVXRUI+xznTOVthj1aroBHGOe3/1t9h1KaSWEP6GzMI59L8KtKJPsFYgTFTvQpRmHK8lTR4by9VAJeA==",
"version": "0.168.0",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.168.0.tgz",
"integrity": "sha512-YMlc+6vvyDPqWKOpzmyifJXBbwlNdqznuy8YBHxX1/90F8d+NnhsxMe1u/ok5LNvNJVJ2TVMkWudu0BUKOSawA==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/flow-remove-types": {
"version": "2.167.1",
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.167.1.tgz",
"integrity": "sha512-R9TUXoC+6FPEE/v775DljM3Ro0xOiPwa0wUy9t8sQ9goNMqlePFR6w8YHAPENg4EXZIU4x/Jfyi6ocYIsJcSWw==",
"version": "2.168.0",
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.168.0.tgz",
"integrity": "sha512-WNXmqPtxgm94OLzvVLokX/MMDrbfs5snI4QDc/up/mx746+aLuM9L+dFUqGg4u4GSwDa5CR69aqT2Zyn9t3AZA==",
"dependencies": {
"flow-parser": "^0.167.1",
"flow-parser": "^0.168.0",
"pirates": "^3.0.2",
"vlq": "^0.2.1"
},
@ -6412,9 +6359,9 @@
}
},
"node_modules/hls.js": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.1.1.tgz",
"integrity": "sha512-2VEVzO/gr5LOD6K/DEmBkKEary6hr4YZ/SLb0PV81jOAM/Tl9DviL0sFMoUHDq05/j4OZxIj691Zo0p40u3Gtw=="
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.1.2.tgz",
"integrity": "sha512-ujditC4vvBmZd00RRNfNPLgFVlqEeUX4sAFv5lGhBHuql8iAZodOdlZTD3em/1zo7vyjQp12up/lCVqQk8dvxA=="
},
"node_modules/hpkp": {
"version": "2.0.0",
@ -8362,9 +8309,9 @@
}
},
"node_modules/object-inspect": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.1.tgz",
"integrity": "sha512-If7BjFlpkzzBeV1cqgT3OSWT3azyoxDGajR+iGnFBfVV2EWyDyWaZZW2ERDjUaY2QM8i5jI3Sj7mhsM4DDAqWA==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
"integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -10579,9 +10526,9 @@
}
},
"node_modules/send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
"integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
"dependencies": {
"debug": "2.6.9",
"depd": "~1.1.2",
@ -10590,9 +10537,9 @@
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"http-errors": "1.8.1",
"mime": "1.6.0",
"ms": "2.1.1",
"ms": "2.1.3",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
@ -10614,21 +10561,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
},
"node_modules/send/node_modules/http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"dependencies": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/send/node_modules/mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
@ -10641,22 +10573,9 @@
}
},
"node_modules/send/node_modules/ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"node_modules/send/node_modules/setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"node_modules/send/node_modules/toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==",
"engines": {
"node": ">=0.6"
}
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/serialize-javascript": {
"version": "6.0.0",
@ -10746,14 +10665,14 @@
"integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
},
"node_modules/serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
"integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
"dependencies": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
"send": "0.17.2"
},
"engines": {
"node": ">= 0.8.0"
@ -11473,11 +11392,11 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.5.tgz",
"integrity": "sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz",
"integrity": "sha512-LPIisi3Ol4chwAaPP8toUJ3L4qCM1G0wao7L3qNv57Drezxj6+VEyySpPw4B1HSO2Eg/hDY/MNF5XihCAoqnsQ==",
"dependencies": {
"jest-worker": "^27.0.6",
"jest-worker": "^27.4.1",
"schema-utils": "^3.1.1",
"serialize-javascript": "^6.0.0",
"source-map": "^0.6.1",
@ -14078,9 +13997,9 @@
}
},
"@types/eslint-scope": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.1.tgz",
"integrity": "sha512-SCFeogqiptms4Fg29WpOTk5nHIzfpKCemSN63ksBQYKTcXoJEmJagV+DhVmbapZzY4/5YaOV1nZwrsU79fFm1g==",
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.2.tgz",
"integrity": "sha512-TzgYCWoPiTeRg6RQYgtuW7iODtVoKu3RVL72k3WohqhjfaOLK5Mg2T4Tg1o2bSfu0vPkoI48wdQFv5b/Xe04wQ==",
"requires": {
"@types/eslint": "*",
"@types/estree": "*"
@ -14995,9 +14914,9 @@
}
},
"caniuse-lite": {
"version": "1.0.30001287",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001287.tgz",
"integrity": "sha512-4udbs9bc0hfNrcje++AxBuc6PfLNHwh3PO9kbwnfCQWyqtlzg3py0YgFu8jyRTTo85VAz4U+VLxSlID09vNtWA=="
"version": "1.0.30001289",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001289.tgz",
"integrity": "sha512-hV6x4IfrYViN8cJbGFVbjD7KCrhS/O7wfDgvevYRanJ/IN+hhxpTcXXqaxy3CzPNFe5rlqdimdEB/k7H0YzxHg=="
},
"chai": {
"version": "4.3.4",
@ -15135,9 +15054,9 @@
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"colord": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.1.tgz",
"integrity": "sha512-4LBMSt09vR0uLnPVkOUBnmxgoaeN4ewRbx801wY/bXcltXfpR/G46OdWn96XpYmCWuYvO46aBZP4NgX8HpNAcw=="
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/colord/-/colord-2.9.2.tgz",
"integrity": "sha512-Uqbg+J445nc1TKn4FoDPS6ZZqAvEDnwrH42yo8B40JSOgSLxMZ/gt3h4nmCtPLQeXhjJJkqBx7SCY35WnIixaQ=="
},
"colorette": {
"version": "2.0.16",
@ -15282,11 +15201,18 @@
}
},
"content-disposition": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz",
"integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==",
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"requires": {
"safe-buffer": "5.1.2"
"safe-buffer": "5.2.1"
},
"dependencies": {
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
"content-security-policy-builder": {
@ -15873,9 +15799,9 @@
"integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA=="
},
"electron-to-chromium": {
"version": "1.4.20",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.20.tgz",
"integrity": "sha512-N7ZVNrdzX8NE90OXEFBMsBf3fp8P/vVDUER3WCUZjzC7OkNTXHVoF6W9qVhq8+dA8tGnbDajzUpj2ISNVVyj+Q=="
"version": "1.4.24",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.24.tgz",
"integrity": "sha512-erwx5r69B/WFfFuF2jcNN0817BfDBdC4765kQ6WltOMuwsimlQo3JTEq0Cle+wpHralwdeX3OfAtw/mHxPK0Wg=="
},
"emoji-regex": {
"version": "8.0.0",
@ -16748,16 +16674,16 @@
}
},
"express": {
"version": "4.17.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
"integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
"version": "4.17.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.17.2.tgz",
"integrity": "sha512-oxlxJxcQlYwqPWKVJJtvQiwHgosH/LrLSPA+H4UxpyvSS6jC5aH+5MoHFM+KABgTOt0APue4w66Ha8jCUo9QGg==",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"body-parser": "1.19.1",
"content-disposition": "0.5.4",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie": "0.4.1",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
@ -16771,46 +16697,19 @@
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"proxy-addr": "~2.0.7",
"qs": "6.9.6",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"safe-buffer": "5.2.1",
"send": "0.17.2",
"serve-static": "1.14.2",
"setprototypeof": "1.2.0",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
},
"dependencies": {
"body-parser": {
"version": "1.19.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
"integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
"requires": {
"bytes": "3.1.0",
"content-type": "~1.0.4",
"debug": "2.6.9",
"depd": "~1.1.2",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"on-finished": "~2.3.0",
"qs": "6.7.0",
"raw-body": "2.4.0",
"type-is": "~1.6.17"
}
},
"bytes": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
"integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
},
"cookie": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
"integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
},
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@ -16819,23 +16718,6 @@
"ms": "2.0.0"
}
},
"http-errors": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
"integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.3",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
@ -16846,31 +16728,10 @@
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
"integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
},
"qs": {
"version": "6.7.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
"integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
},
"raw-body": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
"integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
"requires": {
"bytes": "3.1.0",
"http-errors": "1.7.2",
"iconv-lite": "0.4.24",
"unpipe": "1.0.0"
}
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
"safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
}
}
},
@ -17142,16 +17003,16 @@
"integrity": "sha512-8/sOawo8tJ4QOBX8YlQBMxL8+RLZfxMQOif9o0KUKTNTjMYElWPE0r/m5VNFxTRd0NSw8qSy8dajrwX4RYI1Hw=="
},
"flow-parser": {
"version": "0.167.1",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.167.1.tgz",
"integrity": "sha512-KQGKp9VvVXRUI+xznTOVthj1aroBHGOe3/1t9h1KaSWEP6GzMI59L8KtKJPsFYgTFTvQpRmHK8lTR4by9VAJeA=="
"version": "0.168.0",
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.168.0.tgz",
"integrity": "sha512-YMlc+6vvyDPqWKOpzmyifJXBbwlNdqznuy8YBHxX1/90F8d+NnhsxMe1u/ok5LNvNJVJ2TVMkWudu0BUKOSawA=="
},
"flow-remove-types": {
"version": "2.167.1",
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.167.1.tgz",
"integrity": "sha512-R9TUXoC+6FPEE/v775DljM3Ro0xOiPwa0wUy9t8sQ9goNMqlePFR6w8YHAPENg4EXZIU4x/Jfyi6ocYIsJcSWw==",
"version": "2.168.0",
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.168.0.tgz",
"integrity": "sha512-WNXmqPtxgm94OLzvVLokX/MMDrbfs5snI4QDc/up/mx746+aLuM9L+dFUqGg4u4GSwDa5CR69aqT2Zyn9t3AZA==",
"requires": {
"flow-parser": "^0.167.1",
"flow-parser": "^0.168.0",
"pirates": "^3.0.2",
"vlq": "^0.2.1"
},
@ -17482,9 +17343,9 @@
"integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg=="
},
"hls.js": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.1.1.tgz",
"integrity": "sha512-2VEVzO/gr5LOD6K/DEmBkKEary6hr4YZ/SLb0PV81jOAM/Tl9DviL0sFMoUHDq05/j4OZxIj691Zo0p40u3Gtw=="
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.1.2.tgz",
"integrity": "sha512-ujditC4vvBmZd00RRNfNPLgFVlqEeUX4sAFv5lGhBHuql8iAZodOdlZTD3em/1zo7vyjQp12up/lCVqQk8dvxA=="
},
"hpkp": {
"version": "2.0.0",
@ -18912,9 +18773,9 @@
"integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
},
"object-inspect": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.1.tgz",
"integrity": "sha512-If7BjFlpkzzBeV1cqgT3OSWT3azyoxDGajR+iGnFBfVV2EWyDyWaZZW2ERDjUaY2QM8i5jI3Sj7mhsM4DDAqWA=="
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.0.tgz",
"integrity": "sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g=="
},
"object-keys": {
"version": "1.1.1",
@ -20405,9 +20266,9 @@
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
},
"send": {
"version": "0.17.1",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
"integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
"version": "0.17.2",
"resolved": "https://registry.npmjs.org/send/-/send-0.17.2.tgz",
"integrity": "sha512-UJYB6wFSJE3G00nEivR5rgWp8c2xXvJ3OPWPhmuteU0IKj8nKbG3DrjiOmLwpnHGYWAVwA69zmTm++YG0Hmwww==",
"requires": {
"debug": "2.6.9",
"depd": "~1.1.2",
@ -20416,9 +20277,9 @@
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"fresh": "0.5.2",
"http-errors": "~1.7.2",
"http-errors": "1.8.1",
"mime": "1.6.0",
"ms": "2.1.1",
"ms": "2.1.3",
"on-finished": "~2.3.0",
"range-parser": "~1.2.1",
"statuses": "~1.5.0"
@ -20439,37 +20300,15 @@
}
}
},
"http-errors": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz",
"integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==",
"requires": {
"depd": "~1.1.2",
"inherits": "2.0.4",
"setprototypeof": "1.1.1",
"statuses": ">= 1.5.0 < 2",
"toidentifier": "1.0.0"
}
},
"mime": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
},
"setprototypeof": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
"integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
},
"toidentifier": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
"integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
}
}
},
@ -20556,14 +20395,14 @@
}
},
"serve-static": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
"integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
"version": "1.14.2",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.2.tgz",
"integrity": "sha512-+TMNA9AFxUEGuC0z2mevogSnn9MXKb4fa7ngeRMJaaGv8vTwnIEkKi+QGvPt33HSnf8pRS+WGM0EbMtCJLKMBQ==",
"requires": {
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
"send": "0.17.1"
"send": "0.17.2"
}
},
"server": {
@ -21126,11 +20965,11 @@
}
},
"terser-webpack-plugin": {
"version": "5.2.5",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.2.5.tgz",
"integrity": "sha512-3luOVHku5l0QBeYS8r4CdHYWEGMmIj3H1U64jgkdZzECcSOJAyJ9TjuqcQZvw1Y+4AOBN9SeYJPJmFn2cM4/2g==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.0.tgz",
"integrity": "sha512-LPIisi3Ol4chwAaPP8toUJ3L4qCM1G0wao7L3qNv57Drezxj6+VEyySpPw4B1HSO2Eg/hDY/MNF5XihCAoqnsQ==",
"requires": {
"jest-worker": "^27.0.6",
"jest-worker": "^27.4.1",
"schema-utils": "^3.1.1",
"serialize-javascript": "^6.0.0",
"source-map": "^0.6.1",

View file

@ -17,6 +17,8 @@
"acceptance-firefox": "testcafe firefox:headless --skip-js-errors --quarantine-mode --selector-timeout 5000 -S -s tests/screenshots tests/acceptance",
"acceptance-private": "testcafe chrome:headless --skip-js-errors --quarantine-mode --selector-timeout 5000 -S -s tests/screenshots tests/acceptance-private",
"acceptance-private-firefox": "testcafe firefox:headless --skip-js-errors --quarantine-mode --selector-timeout 5000 -S -s tests/screenshots tests/acceptance-private",
"acceptance-openid": "testcafe chrome:headless --skip-js-errors --quarantine-mode --selector-timeout 5000 -S -s tests/screenshots tests/acceptance-openid",
"acceptance-openid-firefox": "testcafe firefox:headless --skip-js-errors --quarantine-mode --selector-timeout 5000 -S -s tests/screenshots tests/acceptance-openid",
"acceptance-local": "testcafe chrome --selector-timeout 5000 -S -s tests/screenshots tests/acceptance",
"gettext-extract": "gettext-extract --output src/locales/translations.pot $(find src -type f \\( -iname \\*.vue -o -iname \\*.js \\) -not -path src/common/vm.js)",
"gettext-compile": "gettext-compile --output src/locales/translations.json src/locales/*.po"

View file

@ -57,6 +57,7 @@ import Hls from "hls.js";
import "common/maptiler-lang";
import { $gettext, Mount } from "common/vm";
import * as offline from "@lcdp/offline-plugin/runtime";
import { aclMixin } from "./common/acl";
// Initialize helpers
const viewer = new Viewer();
@ -100,6 +101,8 @@ Vue.prototype.$earlyAccess = () => {
});
};
Vue.mixin(aclMixin);
// Register Vuetify
Vue.use(Vuetify, { rtl, icons, theme });

128
frontend/src/common/acl.js Normal file
View file

@ -0,0 +1,128 @@
import { config } from "../session";
export const Constants = {
roles: {
RoleDefault: "*", // used if no role matches
RoleAdmin: "admin",
RolePartner: "partner",
RoleFamily: "family",
RoleSibling: "sibling",
RoleParent: "parent",
RoleGrandparent: "grandparent",
RoleChild: "child",
RoleFriend: "friend",
RoleBestFriend: "best-friend",
RoleClassmate: "classmate",
RoleWorkmate: "workmate",
RoleGuest: "guest",
RoleMember: "member",
},
actions: {
ActionDefault: "*", // allows a subject/role to execute all other actions
ActionSearch: "search",
ActionCreate: "create",
ActionRead: "read",
ActionUpdate: "update",
ActionUpdateSelf: "update-self",
ActionDelete: "delete",
ActionArchive: "archive",
ActionPrivate: "private",
ActionUpload: "upload",
ActionDownload: "download",
ActionShare: "share",
ActionLike: "like",
ActionComment: "comment",
ActionExport: "export",
ActionImport: "import",
},
resources: {
ResourceDefault: "*",
ResourceConfig: "config",
ResourceConfigOptions: "config_options",
ResourceSettings: "settings",
ResourceLogs: "logs",
ResourceAccounts: "accounts",
ResourceSubjects: "subjects",
ResourceAlbums: "albums",
ResourceCameras: "cameras",
ResourceCategories: "categories",
ResourceCountries: "countries",
ResourceFiles: "files",
ResourceFolders: "folders",
ResourceLabels: "labels",
ResourceLenses: "lenses",
ResourceLinks: "links",
ResourceGeo: "geo",
ResourcePasswords: "passwords",
ResourceUsers: "users",
ResourcePhotos: "photos",
ResourcePlaces: "places",
ResourceFeedback: "feedback",
ResourceReview: "review",
ResourceArchive: "archive",
ResourcePrivate: "private",
ResourceLibrary: "library",
},
};
export default class Acl {
constructor(acl) {
this.acl = acl;
}
accessAllowed(role, resource, action) {
if (!this.acl) return false;
let res;
if (!this.acl[resource]) {
if (!this.acl[Constants.resources.ResourceDefault]) return false;
res = this.acl[Constants.resources.ResourceDefault];
} else {
res = this.acl[resource];
}
let rol;
if (!res[role]) {
if (!res[Constants.roles.RoleDefault]) return false;
rol = res[Constants.roles.RoleDefault];
} else {
rol = res[role];
}
let act;
if (!rol[action]) {
if (!rol[Constants.actions.ActionDefault]) return false;
act = rol[Constants.actions.ActionDefault];
} else {
act = rol[action];
}
return act;
}
accessAllowedAny(role, resource, ...actions) {
return actions.some((action) => {
return this.accessAllowed(role, resource, action);
});
}
}
export const aclMixin = {
data() {
return {
aclResources: Constants.resources,
aclActions: Constants.actions,
};
},
computed: {
acl() {
const c = config.values.acl ? config.values : window.__CONFIG__;
return new Acl(c.acl);
},
},
methods: {
hasPermission(resource, ...actions) {
const c = config.values.acl ? config.values : window.__CONFIG__;
console.log(c);
if (c.public) return true;
const role = this.$session.getUser().getRole();
return this.acl.accessAllowedAny(role, resource, ...actions);
},
},
};

View file

@ -232,6 +232,7 @@ export default class Session {
}
logout(noRedirect) {
sessionStorage.setItem("preventAutoLogin", Date.now().toString());
if (this.hasId()) {
return Api.delete("session/" + this.getId())
.then(() => {

View file

@ -22,7 +22,7 @@
</template>
<v-btn
v-if="features.share"
v-if="features.share && hasPermission(aclResources.ResourceAlbums, aclActions.ActionShare)"
fab dark small
:title="$gettext('Share')"
color="share"
@ -33,6 +33,7 @@
<v-icon>share</v-icon>
</v-btn>
<v-btn
v-if="hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate)"
fab dark small
:title="$gettext('Edit')"
color="edit"
@ -54,7 +55,7 @@
<v-icon>get_app</v-icon>
</v-btn>
<v-btn
v-if="features.albums"
v-if="features.albums && hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate, aclActions.ActionCreate)"
fab dark small
:title="$gettext('Add to album')"
color="album"
@ -65,7 +66,7 @@
<v-icon>bookmark</v-icon>
</v-btn>
<v-btn
v-if="deletable.includes(context)"
v-if="deletable.includes(context) && hasPermission(aclResources.ResourceAlbums, aclActions.ActionDelete)"
fab dark small
color="remove"
:title="$gettext('Delete')"

View file

@ -13,11 +13,11 @@
<v-icon>refresh</v-icon>
</v-btn>
<v-btn icon class="action-edit" :title="$gettext('Edit')" @click.stop="dialog.edit = true">
<v-btn v-if="hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate)" icon class="action-edit" :title="$gettext('Edit')" @click.stop="dialog.edit = true">
<v-icon>edit</v-icon>
</v-btn>
<v-btn v-if="$config.feature('share')" icon class="action-share" :title="$gettext('Share')"
<v-btn v-if="$config.feature('share') && hasPermission(aclResources.ResourceAlbums, aclActions.ActionShare)" icon class="action-share" :title="$gettext('Share')"
@click.stop="dialog.share = true">
<v-icon>share</v-icon>
</v-btn>
@ -27,18 +27,18 @@
<v-icon>get_app</v-icon>
</v-btn>
<v-btn v-if="settings.view === 'cards'" icon :title="$gettext('Toggle View')" @click.stop="setView('list')">
<v-btn v-if="settings.view === 'cards'" icon class="action-list-view" :title="$gettext('Toggle View')" @click.stop="setView('list')">
<v-icon>view_list</v-icon>
</v-btn>
<v-btn v-else-if="settings.view === 'list'" icon :title="$gettext('Toggle View')" @click.stop="setView('mosaic')">
<v-btn v-else-if="settings.view === 'list'" icon class="action-mosaic-view" :title="$gettext('Toggle View')" @click.stop="setView('mosaic')">
<v-icon>view_comfy</v-icon>
</v-btn>
<v-btn v-else icon :title="$gettext('Toggle View')" @click.stop="setView('cards')">
<v-btn v-else icon class="action-card-view" :title="$gettext('Toggle View')" @click.stop="setView('cards')">
<v-icon>view_column</v-icon>
</v-btn>
<v-btn v-if="!$config.values.readonly && $config.feature('upload')" icon class="hidden-sm-and-down action-upload"
:title="$gettext('Upload')" @click.stop="showUpload()">
<v-btn v-if="!$config.values.readonly && $config.feature('upload') && hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpload)"
icon class="hidden-sm-and-down action-upload" :title="$gettext('Upload')" @click.stop="showUpload()">
<v-icon>cloud_upload</v-icon>
</v-btn>
</v-toolbar>

View file

@ -36,7 +36,7 @@
</v-btn>
<v-btn
v-if="$config.feature('albums')"
v-if="$config.feature('albums') && hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate, aclActions.ActionCreate)"
fab dark small
:title="$gettext('Add to album')"
color="album"

View file

@ -36,7 +36,7 @@
<v-icon>cloud_download</v-icon>
</v-btn -->
<v-btn
v-if="$config.feature('albums')"
v-if="$config.feature('albums') && hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate, aclActions.ActionCreate)"
fab dark small
:title="$gettext('Add to album')"
color="album"
@ -47,6 +47,7 @@
<v-icon>bookmark</v-icon>
</v-btn>
<v-btn
v-if="hasPermission(aclResources.ResourceLabels, aclActions.ActionDelete)"
fab dark small
color="remove"
:title="$gettext('Delete')"

View file

@ -12,7 +12,7 @@
<template v-else>{{ page.title }}</template>
</v-toolbar-title>
<v-btn v-if="auth && !config.readonly && $config.feature('upload')" icon class="action-upload"
<v-btn v-if="auth && !config.readonly && $config.feature('upload') && hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpload)" icon class="action-upload"
:title="$gettext('Upload')" @click.stop="openUpload()">
<v-icon>cloud_upload</v-icon>
</v-btn>
@ -118,7 +118,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile v-if="$config.feature('review')" to="/review" class="nav-review"
<v-list-tile v-if="$config.feature('review') && hasPermission(aclResources.ResourceReview, aclActions.ActionRead)" to="/review" class="nav-review"
@click.stop="">
<v-list-tile-content>
<v-list-tile-title :class="`p-flex-menuitem menu-item ${rtl ? '--rtl' : ''}`">
@ -128,7 +128,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile v-show="$config.feature('archive')" to="/archive" class="nav-archive" @click.stop="">
<v-list-tile v-show="$config.feature('archive') && hasPermission(aclResources.ResourceArchive, aclActions.ActionRead)" to="/archive" class="nav-archive" @click.stop="">
<v-list-tile-content>
<v-list-tile-title :class="`p-flex-menuitem menu-item ${rtl ? '--rtl' : ''}`">
<translate>Archive</translate>
@ -204,7 +204,7 @@
</v-list-tile>
</v-list-group>
<v-list-tile v-show="$config.feature('people')" :to="{ name: 'people' }" class="nav-people" @click.stop="">
<v-list-tile v-show="$config.feature('people') && hasPermission(aclResources.ResourceSubjects, aclActions.ActionSearch, aclActions.ActionRead)" :to="{ name: 'people' }" class="nav-people" @click.stop="">
<v-list-tile-action :title="$gettext('People')">
<v-icon>person</v-icon>
</v-list-tile-action>
@ -296,7 +296,7 @@
</v-list-tile>
</v-list-group>
<v-list-tile v-show="$config.feature('labels')" to="/labels" class="nav-labels" @click.stop="">
<v-list-tile v-show="$config.feature('labels') && hasPermission(aclResources.ResourceLabels, aclActions.ActionRead, aclActions.ActionSearch)" to="/labels" class="nav-labels" @click.stop="">
<v-list-tile-action :title="$gettext('Labels')">
<v-icon>label</v-icon>
</v-list-tile-action>
@ -324,7 +324,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile v-show="$config.feature('private')" to="/private" class="nav-private" @click.stop="">
<v-list-tile v-show="$config.feature('private') && hasPermission(aclResources.ResourcePrivate, aclActions.ActionRead)" to="/private" class="nav-private" @click.stop="">
<v-list-tile-action :title="$gettext('Private')">
<v-icon>lock</v-icon>
</v-list-tile-action>
@ -337,7 +337,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile v-if="isMini && $config.feature('library')" to="/library" class="nav-library" @click.stop="">
<v-list-tile v-if="isMini && $config.feature('library') && hasPermission(aclResources.ResourceLibrary, aclActions.ActionRead)" to="/library" class="nav-library" @click.stop="">
<v-list-tile-action :title="$gettext('Library')">
<v-icon>camera_roll</v-icon>
</v-list-tile-action>
@ -349,7 +349,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-group v-if="!isMini && $config.feature('library')" prepend-icon="camera_roll" no-action>
<v-list-group v-if="!isMini && $config.feature('library') && hasPermission(aclResources.ResourceLibrary, aclActions.ActionRead)" prepend-icon="camera_roll" no-action>
<template #activator>
<v-list-tile to="/library" class="nav-library" @click.stop="">
<v-list-tile-content>
@ -387,7 +387,7 @@
</v-list-tile>
</v-list-group>
<template v-if="!config.disable.settings">
<template v-if="!config.disable.settings && hasPermission(aclResources.ResourceSettings, aclActions.ActionRead)">
<v-list-tile v-if="isMini" to="/settings" class="nav-settings" @click.stop="">
<v-list-tile-action :title="$gettext('Settings')">
<v-icon>settings</v-icon>
@ -466,7 +466,7 @@
</v-list-tile-content>
</v-list-tile>
<v-list-tile v-show="!isPublic && auth" to="/settings/account" class="p-profile">
<v-list-tile v-show="!isPublic && auth" to="/account" class="nav-profile">
<v-list-tile-avatar color="grey" size="36">
<span class="white--text headline">{{ displayName.length >= 1 ? displayName[0].toUpperCase() : "E" }}</span>
</v-list-tile-avatar>
@ -475,11 +475,11 @@
<v-list-tile-title>
{{ displayName }}
</v-list-tile-title>
<v-list-tile-sub-title>{{ accountInfo }}</v-list-tile-sub-title>
<v-list-tile-sub-title style="font-size: smaller">{{ accountInfo }}</v-list-tile-sub-title>
</v-list-tile-content>
<v-list-tile-action :title="$gettext('Logout')">
<v-btn icon @click="logout">
<v-btn icon @click.stop.prevent="logout">
<v-icon>power_settings_new</v-icon>
</v-btn>
</v-list-tile-action>

View file

@ -83,7 +83,7 @@
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
<v-btn v-if="hidePrivate" :ripple="false"
<v-btn v-if="hidePrivate && hasPermission(aclResources.ResourcePhotos, aclActions.ActionPrivate)" :ripple="false"
icon flat absolute
class="input-private">
<v-icon color="white" class="select-on">lock</v-icon>
@ -104,9 +104,10 @@
icon flat absolute
class="input-favorite"
@touchstart.stop.prevent="input.touchStart($event, index)"
@touchend.stop.prevent="toggleLike($event, index)"
@touchend.stop.prevent="hasPermission(aclResources.ResourcePhotos, aclActions.ActionLike) && toggleLike($event, index)"
@touchmove.stop.prevent
@click.stop.prevent="toggleLike($event, index)">
@click.stop.prevent="hasPermission(aclResources.ResourcePhotos, aclActions.ActionLike) && toggleLike($event, index)"
>
<v-icon color="white" class="select-on">favorite</v-icon>
<v-icon color="white" class="select-off">favorite_border</v-icon>
</v-btn>

View file

@ -23,7 +23,7 @@
</template>
<v-btn
v-if="context !== 'archive' && context !== 'review' && features.share" fab dark
v-if="context !== 'archive' && context !== 'review' && features.share && hasPermission(aclResources.ResourcePhotos, aclActions.ActionShare)" fab dark
small
:title="$gettext('Share')"
color="share"
@ -57,7 +57,7 @@
<v-icon>edit</v-icon>
</v-btn>
<v-btn
v-if="context !== 'archive' && features.private" fab dark
v-if="context !== 'archive' && features.private && hasPermission(aclResources.ResourcePhotos, aclActions.ActionPrivate)" fab dark
small
:title="$gettext('Change private flag')"
color="private"
@ -78,7 +78,7 @@
<v-icon>get_app</v-icon>
</v-btn>
<v-btn
v-if="context !== 'archive' && features.albums" fab dark
v-if="context !== 'archive' && features.albums && hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate, aclActions.ActionCreate)" fab dark
small
:title="$gettext('Add to album')"
color="album"
@ -89,7 +89,7 @@
<v-icon>bookmark</v-icon>
</v-btn>
<v-btn
v-if="!isAlbum && context !== 'archive' && features.archive" fab dark
v-if="!isAlbum && context !== 'archive' && features.archive && hasPermission(aclResources.ResourcePhotos, aclActions.ActionArchive)" fab dark
small
color="remove"
:title="$gettext('Archive')"
@ -100,7 +100,7 @@
<v-icon>archive</v-icon>
</v-btn>
<v-btn
v-if="!album && context === 'archive'" fab dark
v-if="!album && context === 'archive' && hasPermission(aclResources.ResourcePhotos, aclActions.ActionArchive)" fab dark
small
color="restore"
:title="$gettext('Restore')"
@ -111,7 +111,7 @@
<v-icon>unarchive</v-icon>
</v-btn>
<v-btn
v-if="isAlbum && features.albums" fab dark
v-if="isAlbum && features.albums && hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate)" fab dark
small
:title="$gettext('Remove from album')"
color="remove"
@ -122,7 +122,7 @@
<v-icon>eject</v-icon>
</v-btn>
<v-btn
v-if="!album && context === 'archive' && features.delete" fab dark
v-if="!album && context === 'archive' && features.delete && hasPermission(aclResources.ResourcePhotos, aclActions.ActionDelete)" fab dark
small
:title="$gettext('Delete')"
color="remove"

View file

@ -93,13 +93,13 @@
</span>
</td>
<td class="text-xs-center">
<v-btn v-if="hidePrivate" class="input-private" icon small flat :ripple="false"
<v-btn v-if="hidePrivate" class="input-private" icon small flat :ripple="false" :disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionPrivate)"
:data-uid="props.item.UID" @click.stop.prevent="props.item.togglePrivate()">
<v-icon v-if="props.item.Private" color="secondary-dark" class="select-on">lock</v-icon>
<v-icon v-else color="secondary" class="select-off">lock_open</v-icon>
</v-btn>
<v-btn class="input-like" icon small flat :ripple="false"
:data-uid="props.item.UID" @click.stop.prevent="props.item.toggleLike()">
<v-btn class="input-like" icon small flat :ripple="false" :disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionLike)"
:data-uid="props.item.UID" @click.stop.prevent="hasPermission(aclResources.ResourcePhotos, aclActions.ActionLike) && props.item.toggleLike()">
<v-icon v-if="props.item.Favorite" color="pink lighten-3" :data-uid="props.item.UID" class="select-on">
favorite
</v-icon>

View file

@ -82,7 +82,7 @@
<v-icon color="white" class="action-play">play_arrow</v-icon>
</v-btn>
<v-btn v-if="hidePrivate" :ripple="false"
<v-btn v-if="hidePrivate && hasPermission(aclResources.ResourcePhotos, aclActions.ActionPrivate)" :ripple="false"
icon flat small absolute
class="input-private">
<v-icon color="white" class="select-on">lock</v-icon>
@ -103,9 +103,9 @@
icon flat small absolute
class="input-favorite"
@touchstart.stop.prevent="input.touchStart($event, index)"
@touchend.stop.prevent="toggleLike($event, index)"
@touchend.stop.prevent="hasPermission(aclResources.ResourcePhotos, aclActions.ActionLike) && toggleLike($event, index)"
@touchmove.stop.prevent
@click.stop.prevent="toggleLike($event, index)">
@click.stop.prevent="hasPermission(aclResources.ResourcePhotos, aclActions.ActionLike) && toggleLike($event, index)">
<v-icon color="white" class="select-on">favorite</v-icon>
<v-icon color="white" class="select-off">favorite_border</v-icon>
</v-btn>
@ -220,7 +220,7 @@ export default {
},
selectRange(index) {
this.$clipboard.addRange(index, this.photos);
}
},
},
};
</script>

View file

@ -28,7 +28,7 @@
<v-icon>view_column</v-icon>
</v-btn>
<v-btn v-if="!$config.values.readonly && $config.feature('upload')" icon class="hidden-sm-and-down action-upload"
<v-btn v-if="!$config.values.readonly && $config.feature('upload') && hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpload)" icon class="hidden-sm-and-down action-upload"
:title="$gettext('Upload')" @click.stop="showUpload()">
<v-icon>cloud_upload</v-icon>
</v-btn>

View file

@ -33,7 +33,7 @@
<v-icon v-else size="16" color="white">radio_button_off</v-icon>
</button>
<button class="pswp__button action-like hidden-shared-only" style="background: none;"
<button v-if="hasPermission(aclResources.ResourcePhotos, aclActions.ActionLike)" class="pswp__button action-like hidden-shared-only" style="background: none;"
:title="$gettext('Like')" @click.exact="onLike">
<v-icon v-if="item.favorite" size="16" color="white">favorite</v-icon>
<v-icon v-else size="16" color="white">favorite_border</v-icon>
@ -269,7 +269,7 @@ export default {
g.close(); // Close Gallery
Event.publish("dialog.edit", {selection, album, index}); // Open Edit Dialog
}
},
}
};
</script>

View file

@ -36,7 +36,7 @@
</v-btn>
<v-btn
v-if="features.albums"
v-if="features.albums && hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate, aclActions.ActionCreate)"
fab dark small
:title="$gettext('Add to album')"
color="album"

View file

@ -407,7 +407,6 @@ export default {
},
data() {
return {
disabled: !this.$config.feature("edit"),
config: this.$config.values,
all: {
colors: [{label: this.$gettext("Unknown"), name: ""}],
@ -430,6 +429,9 @@ export default {
lensOptions() {
return this.config.lenses;
},
disabled() {
return !this.$config.feature("edit") || !this.hasPermission(this.aclResources.ResourcePhotos, this.aclActions.ActionUpdate);
}
},
watch: {
model() {

View file

@ -45,19 +45,21 @@
@click.stop.prevent="downloadFile(file)">
<translate>Download</translate>
</v-btn>
<v-btn v-if="features.edit && file.Type === 'jpg' && !file.Error && !file.Primary" small depressed dark
<v-btn v-if="features.edit && file.Type === 'jpg' && !file.Error && !file.Primary && hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate)"
small depressed dark
color="primary-button"
class="ma-0 action-primary"
@click.stop.prevent="primaryFile(file)">
<translate>Primary</translate>
</v-btn>
<v-btn v-if="features.edit && !file.Sidecar && !file.Error && !file.Primary && file.Root === '/'" small
depressed dark color="primary-button"
<v-btn v-if="features.edit && !file.Sidecar && !file.Error && !file.Primary && file.Root === '/' && hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate)"
small depressed dark color="primary-button"
class="ma-0 action-unstack"
@click.stop.prevent="unstackFile(file)">
<translate>Unstack</translate>
</v-btn>
<v-btn v-if="features.delete && !file.Primary" small depressed dark color="primary-button"
<v-btn v-if="features.delete && !file.Primary && hasPermission(aclResources.ResourceFiles, aclActions.ActionDelete)"
small depressed dark color="primary-button"
class="ma-0 action-delete"
@click.stop.prevent="showDeleteDialog(file)">
<translate>Delete</translate>

View file

@ -18,6 +18,7 @@
</td>
<td>
<v-select
:disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate)"
v-model="model.Type"
flat solo
browser-autocomplete="off"
@ -47,6 +48,7 @@
</td>
<td>
<v-text-field
:disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate)"
v-model="model.OriginalName"
flat solo dense hide-details color="secondary-dark"
@change="save"
@ -100,6 +102,7 @@
</td>
<td>
<v-switch
:disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate)"
v-model="model.Stack"
hide-details
class="input-stackable"
@ -116,6 +119,7 @@
</td>
<td>
<v-switch
:disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionLike)"
v-model="model.Favorite"
hide-details
class="input-favorite"
@ -130,6 +134,7 @@
</td>
<td>
<v-switch
:disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionPrivate)"
v-model="model.Private"
hide-details
class="input-private"
@ -144,6 +149,7 @@
</td>
<td>
<v-switch
:disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate)"
v-model="model.Scan"
hide-details
class="input-scan"
@ -158,6 +164,7 @@
</td>
<td>
<v-switch
:disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate)"
v-model="model.Panorama"
hide-details
class="input-panorama"
@ -205,6 +212,7 @@
</td>
<td>
<v-text-field
:disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate)"
v-model="model.CellAccuracy"
flat solo dense hide-details color="secondary-dark"
type="number"

View file

@ -19,7 +19,7 @@
@save="renameLabel(props.item.Label)"
>
{{ props.item.Label.Name }}
<template #input>
<template v-if="hasPermission(aclResources.ResourceLabels, aclActions.ActionUpdate)" #input>
<v-text-field
v-model="props.item.Label.Name"
:rules="[nameRule]"
@ -40,24 +40,27 @@
<v-icon color="secondary-dark">search</v-icon>
</v-btn>
<v-btn v-else-if="props.item.Uncertainty < 100 && props.item.LabelSrc === 'manual'" icon
:disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate)"
small flat :ripple="false"
class="action-delete" title="Delete"
@click.stop.prevent="removeLabel(props.item.Label)">
<v-icon color="secondary-dark">delete</v-icon>
</v-btn>
<v-btn v-else-if="props.item.Uncertainty < 100" icon small flat :ripple="false"
:disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate)"
class="action-remove" title="Remove"
@click.stop.prevent="removeLabel(props.item.Label)">
<v-icon color="secondary-dark">remove</v-icon>
</v-btn>
<v-btn v-else icon small flat :ripple="false"
:disabled="!hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate)"
class="action-on" title="Activate"
@click.stop.prevent="activateLabel(props.item.Label)">
<v-icon color="secondary-dark">add</v-icon>
</v-btn>
</td>
</template>
<template v-if="!disabled" #footer>
<template v-if="!disabled && hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpdate) && hasPermission(aclResources.ResourceLabels, aclActions.ActionCreate)" #footer>
<td>
<v-text-field
v-model="newLabel"

View file

@ -29,7 +29,7 @@
:transition="false"
aspect-ratio="1"
class="accent lighten-2">
<v-btn v-if="!marker.SubjUID && !marker.Invalid" :ripple="false" :depressed="false" class="input-reject"
<v-btn v-if="!marker.SubjUID && !marker.Invalid && hasPermission(aclResources.ResourceFiles, aclActions.ActionUpdate)" :ripple="false" :depressed="false" class="input-reject"
icon flat small absolute :title="$gettext('Remove')"
@click.stop.prevent="onReject(marker)">
<v-icon color="white" class="action-reject">clear</v-icon>
@ -52,7 +52,7 @@
<v-text-field
v-model="marker.Name"
:rules="[textRule]"
:disabled="busy"
:disabled="busy || !hasPermission(aclResources.ResourceFiles, aclActions.ActionUpdate)"
:readonly="true"
browser-autocomplete="off"
class="input-name pa-0 ma-0"
@ -75,7 +75,7 @@
:items="$config.values.people"
item-value="Name"
item-text="Name"
:disabled="busy"
:disabled="busy || !hasPermission(aclResources.ResourceFiles, aclActions.ActionUpdate)"
:return-object="false"
:menu-props="menuProps"
:allow-overflow="false"

View file

@ -32,6 +32,7 @@ import RestModel from "model/rest";
import Form from "common/form";
import Api from "common/api";
import { $gettext } from "common/vm";
import { Constants } from "../common/acl";
export class User extends RestModel {
getDefaults() {
@ -114,6 +115,19 @@ export class User extends RestModel {
);
}
getRole() {
const roles = Constants.roles;
if (this.RoleAdmin) return roles.RoleAdmin;
if (this.RoleChild) return roles.RoleChild;
if (this.RoleFamily) return roles.RoleFamily;
if (this.RoleFriend) return roles.RoleFriend;
if (this.RoleGuest) return roles.RoleGuest;
if (this.UserName.length >= 4 && this.UID.length === 16 && this.UID[0] === "u") {
return roles.RoleMember;
}
return roles.RoleDefault;
}
static getCollectionResource() {
return "users";
}

View file

@ -34,12 +34,12 @@
<v-icon>refresh</v-icon>
</v-btn>
<v-btn v-if="!$config.values.readonly && $config.feature('upload')" icon class="hidden-sm-and-down action-upload"
<v-btn v-if="!$config.values.readonly && $config.feature('upload') && hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpload)" icon class="hidden-sm-and-down action-upload"
:title="$gettext('Upload')" @click.stop="showUpload()">
<v-icon>cloud_upload</v-icon>
</v-btn>
<v-btn v-if="staticFilter.type === 'album'" icon class="action-add" :title="$gettext('Add Album')"
<v-btn v-if="staticFilter.type === 'album' && hasPermission(aclResources.ResourceAlbums, aclActions.ActionCreate)" icon class="action-add" :title="$gettext('Add Album')"
@click.prevent="create">
<v-icon>add</v-icon>
</v-btn>
@ -101,9 +101,9 @@
@mousedown="input.mouseDown($event, index)"
@click.stop.prevent="onClick($event, index)"
>
<v-btn v-if="featureShare && album.LinkCount > 0" :ripple="false"
<v-btn v-if="featureShare && album.LinkCount > 0 && hasPermission(aclResources.ResourceAlbums, aclActions.ActionShare)" :ripple="false"
icon flat absolute
class="action-share"
class="input-share"
@touchstart.stop.prevent="input.touchStart($event, index)"
@touchend.stop.prevent="onShare($event, index)"
@touchmove.stop.prevent
@ -126,9 +126,9 @@
icon flat absolute
class="input-favorite"
@touchstart.stop.prevent="input.touchStart($event, index)"
@touchend.stop.prevent="toggleLike($event, index)"
@touchend.stop.prevent="hasPermission(aclResources.ResourceAlbums, aclActions.ActionLike) && toggleLike($event, index)"
@touchmove.stop.prevent
@click.stop.prevent="toggleLike($event, index)">
@click.stop.prevent="hasPermission(aclResources.ResourceAlbums, aclActions.ActionLike) && toggleLike($event, index)">
<v-icon color="#FFD600" class="select-on">star</v-icon>
<v-icon color="white" class="select-off">star_border</v-icon>
</v-btn>
@ -138,11 +138,11 @@
<div>
<h3 class="body-2 mb-0">
<button v-if="album.Type !== 'month'" class="action-title-edit" :data-uid="album.UID"
@click.stop.prevent="edit(album)">
@click.stop.prevent="hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate) && edit(album)">
{{ album.Title | truncate(80) }}
</button>
<button v-else class="action-title-edit" :data-uid="album.UID"
@click.stop.prevent="edit(album)">
@click.stop.prevent="hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate) && edit(album)">
{{ album.getDateString() | capitalize }}
</button>
</h3>
@ -152,13 +152,13 @@
<v-card-text primary-title class="pb-2 pt-0 card-details" style="user-select: none;"
@click.stop.prevent="">
<div v-if="album.Description" class="caption mb-2" :title="$gettext('Description')">
<button @click.exact="edit(album)">
<button @click.exact="hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate) && edit(album)">
{{ album.Description | truncate(100) }}
</button>
</div>
<div v-else-if="album.Type === 'album'" class="caption mb-2">
<button v-if="album.PhotoCount === 1" @click.exact="edit(album)">
<button v-if="album.PhotoCount === 1" @click.exact="hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate) && edit(album)">
<translate>Contains one picture.</translate>
</button>
<button v-else-if="album.PhotoCount > 0">
@ -169,18 +169,18 @@
</button>
</div>
<div v-else-if="album.Type === 'folder'" class="caption mb-2">
<button @click.exact="edit(album)">
<button @click.exact="hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate) && edit(album)">
/{{ album.Path | truncate(100) }}
</button>
</div>
<div v-if="album.Category !== ''" class="caption mb-2 d-inline-block">
<button @click.exact="edit(album)">
<button @click.exact="hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate) && edit(album)">
<v-icon size="14">local_offer</v-icon>
{{ album.Category }}
</button>
</div>
<div v-if="album.getLocation() !== ''" class="caption mb-2 d-inline-block">
<button @click.exact="edit(album)">
<button @click.exact="hasPermission(aclResources.ResourceAlbums, aclActions.ActionUpdate) && edit(album)">
<v-icon size="14">location_on</v-icon>
{{ album.getLocation() }}
</button>
@ -189,7 +189,7 @@
</v-card>
</v-flex>
</v-layout>
<div v-if="staticFilter.type === 'album' && config.count.albums === 0" class="text-xs-center my-2">
<div v-if="staticFilter.type === 'album' && config.count.albums === 0 && hasPermission(aclResources.ResourceAlbums, aclActions.ActionCreate)" class="text-xs-center my-2">
<v-btn class="action-add" color="secondary" round @click.prevent="create">
<translate>Add Album</translate>
</v-btn>

View file

@ -94,9 +94,9 @@
icon flat absolute
class="input-favorite"
@touchstart.stop.prevent="input.touchStart($event, index)"
@touchend.stop.prevent="toggleLike($event, index)"
@touchend.stop.prevent="hasPermission(aclResources.ResourceLabels, aclActions.ActionLike) && toggleLike($event, index)"
@touchmove.stop.prevent
@click.stop.prevent="toggleLike($event, index)">
@click.stop.prevent="hasPermission(aclResources.ResourceLabels, aclActions.ActionLike) && toggleLike($event, index)">
<v-icon color="#FFD600" class="select-on">star</v-icon>
<v-icon color="white" class="select-off">star_border</v-icon>
</v-btn>
@ -115,7 +115,7 @@
<span v-else>
<v-icon>edit</v-icon>
</span>
<template #input>
<template v-if="hasPermission(aclResources.ResourceLabels, aclActions.ActionUpdate)" #input>
<v-text-field
v-model="label.Name"
:rules="[titleRule]"

View file

@ -65,7 +65,7 @@
<translate>Cancel</translate>
</v-btn>
<v-btn v-if="!$config.values.readonly && $config.feature('upload')"
<v-btn v-if="!$config.values.readonly && $config.feature('upload') && hasPermission(aclResources.ResourcePhotos, aclActions.ActionUpload)"
:disabled="busy || !ready"
color="primary-button"
class="white--text ml-0 hidden-xs-only action-upload"

View file

@ -19,7 +19,7 @@
browser-autocomplete="off"
color="secondary-dark"
class="input-name"
placeholder="••••••••"
placeholder="username"
></v-text-field>
</v-flex>
<v-flex xs12 class="pa-2">
@ -47,6 +47,18 @@
<translate>Sign in</translate>
<v-icon :right="!rtl" :left="rtl" dark>login</v-icon>
</v-btn>
<v-btn color="primary-button"
class="white--text ml-0 action-confirm"
depressed
:disabled="loading"
@click.stop="loginExternal"
v-if="!!authProvider" >
<translate>Sign in with {{ authProvider }}</translate>
<v-icon :right="!rtl" :left="rtl" dark>login</v-icon>
</v-btn>
<template v-if="authProvider">
<p class="mt-3"> Sign in with local user accounts except admin is disabled when using OpenID Connect.</p>
</template>
</v-flex>
</v-layout>
</v-card-actions>
@ -58,21 +70,54 @@
</template>
<script>
import Notify from "../common/notify";
import axios from "axios";
export default {
name: 'Login',
data() {
const c = this.$config.values;
return {
loading: false,
showPassword: false,
username: "admin",
username: "",
password: "",
siteDescription: c.siteDescription ? c.siteDescription : c.siteCaption,
authProvider: c.oidc ? "OpenID Connect" : null,
nextUrl: this.$route.params.nextUrl ? this.$route.params.nextUrl : "/",
rtl: this.$rtl,
};
},
created() {
const c = window.__CONFIG__;
const preventAutoLogin = sessionStorage.getItem("preventAutoLogin");
const err = window.localStorage.getItem('auth_error');
sessionStorage.removeItem("preventAutoLogin");
if (!c.oidc || this.$route.query.preventAutoLogin || preventAutoLogin) {
if (err) {
Notify.error(err);
window.localStorage.removeItem('auth_error');
}
return;
}
const cleanup = () => {
window.localStorage.removeItem('config');
window.localStorage.removeItem('auth_error');
};
const redirect = () => {
if (err) return;
// check if oidc provider is available
axios.get(c.oidc,{ timeout: 1000}).then(response => {
// redirect to oidc provider
sessionStorage.setItem("preventAutoLogin", Date.now().toString());
window.location.href = '/api/v1/auth/external';
}).catch(error => {
if (c.debug) console.log(error);
});
};
const externalLogin = this.onExternalLogin(cleanup, redirect);
externalLogin();
},
methods: {
login() {
if (!this.username || !this.password) {
@ -87,6 +132,50 @@ export default {
}
).catch(() => this.loading = false);
},
}
loginExternal() {
const c = this.$config.values;
this.loading = true;
axios.get(c.oidc,{ timeout: 3000}).then(response => {
let popup = window.open('api/v1/auth/external', "external-login");
window.localStorage.removeItem('auth_error');
const onstorage = window.onstorage;
const cleanup = () => {
window.localStorage.removeItem('config');
window.localStorage.removeItem('auth_error');
window.onstorage = onstorage;
popup.close();
};
window.onstorage = this.onExternalLogin(cleanup);
}).catch(error => {
if (c.debug) console.log(error);
Notify.error(`Couldn't connect to OpenID Connect Provider. ${error}`);
}).finally(() => {
this.loading = false;
});
},
onExternalLogin(cleanup, redirect) {
return () => {
const sid = window.localStorage.getItem('session_id');
const data = window.localStorage.getItem('data');
const config = window.localStorage.getItem('config');
const error = window.localStorage.getItem('auth_error');
if (error !== null) {
Notify.error(`${error}`);
cleanup();
return;
}
if (sid === null || data === null || config === null) {
if (typeof redirect === 'function') redirect();
return;
}
this.$session.setId(sid);
this.$session.setData(JSON.parse(data));
this.$session.setConfig(JSON.parse(config));
this.$router.push(this.nextUrl);
cleanup();
};
},
},
};
</script>

View file

@ -8,7 +8,7 @@
slider-color="secondary-dark"
:height="$vuetify.breakpoint.smAndDown ? 48 : 64"
>
<v-tab v-for="(item, index) in tabs" :id="'tab-' + item.name" :key="index" :class="item.class"
<v-tab v-for="(item, index) in permittedTabs" :id="'tab-' + item.name" :key="index" :class="item.class"
ripple @click.stop.prevent="changePath(item.path)">
<v-icon v-if="$vuetify.breakpoint.smAndDown" :title="item.label">{{ item.icon }}</v-icon>
<template v-else>
@ -23,7 +23,7 @@
</v-tab>
<v-tabs-items touchless>
<v-tab-item v-for="(item, index) in tabs" :key="index" lazy>
<v-tab-item v-for="(item, index) in permittedTabs" :key="index" lazy>
<component :is="item.component" :static-filter="item.filter" :active="active === index"
@updateFaceCount="onUpdateFaceCount"></component>
</v-tab-item>
@ -53,6 +53,9 @@ export default {
'class': '',
'path': '/people',
'icon': 'people_alt',
'permission': () => {
return this.hasPermission(this.aclResources.ResourceSubjects, this.aclActions.ActionRead, this.aclActions.ActionSearch);
},
},
{
'name': 'people_faces',
@ -63,6 +66,9 @@ export default {
'path': '/people/new',
'icon': 'person_add',
'count': 0,
'permission': () => {
return this.hasPermission(this.aclResources.ResourceSubjects, this.aclActions.ActionUpdate);
},
},
];
@ -76,6 +82,14 @@ export default {
rtl: this.$rtl,
};
},
computed: {
permittedTabs() {
if (!this.tabs) return this.tabs;
return this.tabs.filter(tab => {
return tab.permission();
});
}
},
watch: {
'$route'() {
this.openTab();

View file

@ -9,13 +9,15 @@
<v-btn icon overflow flat depressed color="secondary-dark" class="action-reload" :title="$gettext('Reload')" @click.stop="refresh">
<v-icon>refresh</v-icon>
</v-btn>
<template v-if="">
<v-btn v-if="!filter.hidden" icon class="action-show-hidden" :title="$gettext('Show hidden')" @click.stop="onShowHidden">
<v-icon>visibility</v-icon>
</v-btn>
<v-btn v-else icon class="action-exclude-hidden" :title="$gettext('Exclude hidden')" @click.stop="onExcludeHidden">
<v-icon>visibility_off</v-icon>
</v-btn>
<v-btn v-if="!filter.hidden" icon class="action-show-hidden" :title="$gettext('Show hidden')" @click.stop="onShowHidden">
<v-icon>visibility</v-icon>
</v-btn>
<v-btn v-else icon class="action-exclude-hidden" :title="$gettext('Exclude hidden')" @click.stop="onExcludeHidden">
<v-icon>visibility_off</v-icon>
</v-btn>
</template>
</v-toolbar>
</v-form>
@ -66,6 +68,7 @@
<v-layout v-if="model.SubjUID" row wrap align-center>
<v-flex xs12 class="text-xs-left pa-0">
<v-text-field
v-if="hasPermission(aclResources.ResourceSubjects, aclActions.ActionUpdate)"
v-model="model.Name"
:rules="[textRule]"
:readonly="readonly"

View file

@ -23,13 +23,14 @@
<v-btn icon overflow flat depressed color="secondary-dark" class="action-reload" :title="$gettext('Reload')" @click.stop="refresh">
<v-icon>refresh</v-icon>
</v-btn>
<v-btn v-if="!filter.hidden" icon class="action-show-hidden" :title="$gettext('Show hidden')" @click.stop="onShowHidden">
<v-icon>visibility</v-icon>
</v-btn>
<v-btn v-else icon class="action-exclude-hidden" :title="$gettext('Exclude hidden')" @click.stop="onExcludeHidden">
<v-icon>visibility_off</v-icon>
</v-btn>
<template v-if="hasPermission(aclResources.ResourceSubjects, aclActions.ActionPrivate)">
<v-btn v-if="!filter.hidden" icon class="action-show-hidden" :title="$gettext('Show hidden')" @click.stop="onShowHidden">
<v-icon>visibility</v-icon>
</v-btn>
<v-btn v-else icon class="action-exclude-hidden" :title="$gettext('Exclude hidden')" @click.stop="onExcludeHidden">
<v-icon>visibility_off</v-icon>
</v-btn>
</template>
</v-toolbar>
</v-form>
@ -84,7 +85,7 @@
@mousedown="input.mouseDown($event, index)"
@click.stop.prevent="onClick($event, index)"
>
<v-btn :ripple="false" :depressed="false" class="input-hidden"
<v-btn v-if="hasPermission(aclResources.ResourceSubjects, aclActions.ActionPrivate)" :ripple="false" :depressed="false" class="input-hidden"
icon flat small absolute
@touchstart.stop.prevent="input.touchStart($event, index)"
@touchend.stop.prevent="onToggleHidden($event, index)"
@ -108,9 +109,9 @@
icon flat absolute
class="input-favorite"
@touchstart.stop.prevent="input.touchStart($event, index)"
@touchend.stop.prevent="toggleLike($event, index)"
@touchend.stop.prevent="hasPermission(aclResources.ResourceSubjects, aclActions.ActionLike) && toggleLike($event, index)"
@touchmove.stop.prevent
@click.stop.prevent="toggleLike($event, index)">
@click.stop.prevent="hasPermission(aclResources.ResourceSubjects, aclActions.ActionLike) && toggleLike($event, index)">
<v-icon color="#FFD600" class="select-on">star</v-icon>
<v-icon color="white" class="select-off">star_border</v-icon>
</v-btn>
@ -129,7 +130,7 @@
<span v-else>
<v-icon>edit</v-icon>
</span>
<template #input>
<template v-if="hasPermission(aclResources.ResourceSubjects, aclActions.ActionUpdate)" #input>
<v-text-field
v-model="model.Name"
:rules="[titleRule]"

View file

@ -0,0 +1,94 @@
<template>
<div class="p-page p-page-settings">
<v-tabs
v-model="active"
flat
grow
touchless
color="secondary"
slider-color="secondary-dark"
:height="$vuetify.breakpoint.smAndDown ? 48 : 64"
>
<v-tab v-for="(item, index) in tabs" :id="'tab-' + item.name" :key="index" :class="item.class" ripple
@click="changePath(item.path)">
<v-icon v-if="$vuetify.breakpoint.smAndDown" :title="item.label">{{ item.icon }}</v-icon>
<template v-else>
<v-icon :size="18" :left="!rtl" :right="rtl">{{ item.icon }}</v-icon> {{ item.label }}
</template>
</v-tab>
<v-tabs-items touchless>
<v-tab-item v-for="(item, index) in tabs" :key="index" lazy>
<component :is="item.component"></component>
</v-tab-item>
</v-tabs-items>
</v-tabs>
</div>
</template>
<script>
import Account from "pages/settings/account.vue";
function initTabs(flag, tabs) {
let i = 0;
while(i < tabs.length) {
if(!tabs[i][flag]) {
tabs.splice(i,1);
} else {
i++;
}
}
}
export default {
name: 'PPageProfile',
props: {
tab: String,
},
data() {
const isDemo = this.$config.get("demo");
const isPublic = this.$config.get("public");
const tabs = [
{
'name': 'settings-account',
'component': Account,
'label': this.$gettext('Account'),
'class': '',
'path': '/account',
'icon': 'person',
'public': false,
'admin': true,
'demo': true,
},
];
if(isDemo) {
initTabs("demo", tabs);
} else if(isPublic) {
initTabs("public", tabs);
}
let active = 0;
if (typeof this.tab === 'string' && this.tab !== '') {
active = tabs.findIndex((t) => t.name === this.tab);
}
return {
tabs: tabs,
demo: isDemo,
public: isPublic,
readonly: this.$config.get("readonly"),
active: active,
rtl: this.$rtl,
};
},
methods: {
changePath: function (path) {
if (this.$route.path !== path) {
this.$router.replace(path);
}
}
},
};
</script>

View file

@ -31,7 +31,6 @@ import General from "pages/settings/general.vue";
import Library from "pages/settings/library.vue";
import Advanced from "pages/settings/advanced.vue";
import Sync from "pages/settings/sync.vue";
import Account from "pages/settings/account.vue";
function initTabs(flag, tabs) {
let i = 0;
@ -97,17 +96,6 @@ export default {
'admin': true,
'demo': true,
},
{
'name': 'settings-account',
'component': Account,
'label': this.$gettext('Account'),
'class': '',
'path': '/settings/account',
'icon': 'person',
'public': false,
'admin': true,
'demo': true,
},
];
if(isDemo) {

View file

@ -1,6 +1,6 @@
<template>
<div class="p-tab p-settings-account">
<v-form ref="form" dense class="form-password" accept-charset="UTF-8">
<v-form v-if="!user.ExternalID" ref="form" dense class="form-password" accept-charset="UTF-8">
<v-card flat tile class="ma-2 application">
<v-card-actions>
<v-layout wrap align-top>
@ -68,6 +68,9 @@
</v-card-actions>
</v-card>
</v-form>
<template v-else>
<p class="mt-3 px-4"> Please head to your Identity Provider to manage your account details.</p>
</template>
<p-about-footer></p-about-footer>
</div>
@ -82,6 +85,7 @@ export default {
return {
demo: isDemo,
user: this.$session.getUser(),
oldPassword: "",
newPassword: "",
confirmPassword: "",
@ -89,6 +93,10 @@ export default {
rtl: this.$rtl,
};
},
mounted() {
console.log("this.$session.getUser()");
console.log(this.$session.getUser());
},
methods: {
disabled() {
return (this.demo || this.busy || this.oldPassword === "" || this.newPassword.length < 6 || (this.newPassword !== this.confirmPassword));

View file

@ -44,13 +44,26 @@ import About from "pages/about/about.vue";
import Feedback from "pages/about/feedback.vue";
import License from "pages/about/license.vue";
import Help from "pages/help.vue";
import Profile from "pages/profile.vue";
import { $gettext } from "common/vm";
import { config, session } from "./session";
import Acl, { Constants } from "./common/acl";
const c = window.__CONFIG__;
const appName = c.name;
const siteTitle = c.siteTitle ? c.siteTitle : c.name;
const aclActions = Constants.actions;
const aclResources = Constants.resources;
const hasPermission = (resource, ...actions) => {
const conf = config.values.acl ? config.values : c;
if (conf.public) return true;
const acl = new Acl(conf.acl);
const role = session.getUser().getRole();
return acl.accessAllowedAny(role, resource, ...actions);
};
export default [
{
name: "home",
@ -200,6 +213,13 @@ export default [
component: Photos,
meta: { title: $gettext("Review"), auth: true },
props: { staticFilter: { review: true } },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceReview, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "private",
@ -207,6 +227,13 @@ export default [
component: Photos,
meta: { title: $gettext("Private"), auth: true },
props: { staticFilter: { private: true } },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourcePrivate, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "archive",
@ -214,6 +241,13 @@ export default [
component: Photos,
meta: { title: $gettext("Archive"), auth: true },
props: { staticFilter: { archived: true } },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceArchive, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "places",
@ -245,6 +279,13 @@ export default [
path: "/library/files*",
component: Files,
meta: { title: $gettext("File Browser"), auth: true },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceLibrary, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "hidden",
@ -252,18 +293,41 @@ export default [
component: Photos,
meta: { title: $gettext("Hidden Files"), auth: true },
props: { staticFilter: { hidden: true } },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceLibrary, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "errors",
path: "/library/errors",
component: Errors,
meta: { title: appName, auth: true },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceLibrary, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "labels",
path: "/labels",
component: Labels,
meta: { title: $gettext("Labels"), auth: true },
beforeEnter: (to, from, next) => {
if (
hasPermission(aclResources.ResourceLabels, aclActions.ActionSearch, aclActions.ActionRead)
) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "people",
@ -271,12 +335,24 @@ export default [
component: People,
meta: { title: $gettext("People"), auth: true, background: "application-light" },
beforeEnter: (to, from, next) => {
if (
!hasPermission(
aclResources.ResourceSubjects,
aclActions.ActionRead,
aclActions.ActionSearch
)
) {
next({ name: "home" });
}
if (!config || !from || !from.name || from.name.startsWith("people")) {
next();
} else {
config.load().finally(() => {
// Open new faces tab when there are no people.
if (config.values.count.people === 0) {
if (
config.values.count.people === 0 &&
hasPermission(aclResources.ResourceSubjects, aclActions.ActionUpdate)
) {
next({ name: "people_faces" });
} else {
next();
@ -290,6 +366,13 @@ export default [
path: "/people/new",
component: People,
meta: { title: $gettext("People"), auth: true, background: "application-light" },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceSubjects, aclActions.ActionUpdate)) {
next();
} else {
next({ name: "people" });
}
},
},
{
name: "library",
@ -297,6 +380,13 @@ export default [
component: Library,
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
props: { tab: "library-index" },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceLibrary, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "library_import",
@ -304,6 +394,13 @@ export default [
component: Library,
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
props: { tab: "library-import" },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceLibrary, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "library_logs",
@ -311,6 +408,13 @@ export default [
component: Library,
meta: { title: $gettext("Library"), auth: true, background: "application-light" },
props: { tab: "library-logs" },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceLibrary, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "settings",
@ -323,6 +427,13 @@ export default [
background: "application-light",
},
props: { tab: "settings-general" },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceSettings, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "settings_library",
@ -335,6 +446,13 @@ export default [
background: "application-light",
},
props: { tab: "settings-library" },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceSettings, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "settings_sync",
@ -347,18 +465,31 @@ export default [
background: "application-light",
},
props: { tab: "settings-sync" },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceSettings, aclActions.ActionRead)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "settings_account",
path: "/settings/account",
component: Settings,
name: "profile_account",
path: "/account",
component: Profile,
meta: {
title: $gettext("Settings"),
auth: true,
settings: true,
background: "application-light",
},
props: { tab: "settings-account" },
beforeEnter: (to, from, next) => {
if (hasPermission(aclResources.ResourceUsers, aclActions.ActionUpdateSelf)) {
next();
} else {
next({ name: "home" });
}
},
},
{
name: "settings_advanced",

View file

@ -56,6 +56,7 @@ import VueInfiniteScroll from "vue-infinite-scroll";
import Hls from "hls.js";
import { $gettext, Mount } from "common/vm";
import * as options from "./options/options";
import { aclMixin } from "./common/acl";
// Initialize helpers
const viewer = new Viewer();
@ -90,6 +91,8 @@ Vue.prototype.$clipboard = Clipboard;
Vue.prototype.$isMobile = isMobile;
Vue.prototype.$rtl = rtl;
Vue.mixin(aclMixin);
// Register Vuetify
Vue.use(Vuetify, { rtl, icons, theme });

View file

@ -0,0 +1,208 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import Menu from "../page-model/menu";
import Album from "../page-model/album";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Photo from "../page-model/photo";
import PhotoViewer from "../page-model/photoviewer";
import Page from "../page-model/page";
import AlbumDialog from "../page-model/dialog-album";
fixture`Test albums`.page`${testcafeconfig.url}`;
const menu = new Menu();
const album = new Album();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photo = new Photo();
const photoviewer = new PhotoViewer();
const page = new Page();
const albumdialog = new AlbumDialog();
test.meta("testID", "authentication-000")(
"Time to start instance (will be marked as unstable)",
async (t) => {
await t.wait(5000);
}
);
test.meta("testID", "albums-001").meta({ type: "smoke" })(
"Create/delete album on /albums",
async (t) => {
await menu.openPage("albums");
const AlbumCount = await album.getAlbumCount("all");
await toolbar.triggerToolbarAction("add");
const AlbumCountAfterCreate = await album.getAlbumCount("all");
const NewAlbumUid = await album.getNthAlbumUid("all", 0);
await t.expect(AlbumCountAfterCreate).eql(AlbumCount + 1);
await album.selectAlbumFromUID(NewAlbumUid);
await contextmenu.triggerContextMenuAction("delete", "");
const AlbumCountAfterDelete = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterDelete).eql(AlbumCountAfterCreate - 1);
}
);
test.meta("testID", "albums-002").meta({ type: "smoke" })(
"Create/delete album during add to album",
async (t) => {
await menu.openPage("albums");
const AlbumCount = await album.getAlbumCount("all");
await menu.openPage("browse");
await toolbar.search("photo:true");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
const SecondPhotoUid = await photo.getNthPhotoUid("image", 1);
await photo.selectPhotoFromUID(SecondPhotoUid);
await photo.selectPhotoFromUID(FirstPhotoUid);
await contextmenu.triggerContextMenuAction("album", "NotYetExistingAlbum");
await menu.openPage("albums");
const AlbumCountAfterCreation = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterCreation).eql(AlbumCount + 1);
await toolbar.search("NotYetExistingAlbum");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await album.selectAlbumFromUID(AlbumUid);
await contextmenu.triggerContextMenuAction("delete", "");
await menu.openPage("albums");
const AlbumCountAfterDelete = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterDelete).eql(AlbumCount);
}
);
test.meta("testID", "albums-003").meta({ type: "smoke" })("Update album details", async (t) => {
await menu.openPage("albums");
await toolbar.search("Holiday");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await t.expect(page.cardTitle.nth(0).innerText).contains("Holiday");
await t.click(page.cardTitle.nth(0)).typeText(albumdialog.title, "Animals", { replace: true });
await t.expect(albumdialog.description.value).eql("").expect(albumdialog.category.value).eql("");
await t
.typeText(albumdialog.description, "All my animals")
.typeText(albumdialog.category, "Pets")
.pressKey("enter")
.click(albumdialog.dialogSave);
await t.expect(page.cardTitle.nth(0).innerText).contains("Animals");
await album.openAlbumWithUid(AlbumUid);
await toolbar.triggerToolbarAction("edit");
await t.typeText(albumdialog.title, "Holiday", { replace: true });
await t
.expect(albumdialog.description.value)
.eql("All my animals")
.expect(albumdialog.category.value)
.eql("Pets");
await t
.click(albumdialog.description)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.category)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.dialogSave);
await menu.openPage("albums");
await t
.expect(Selector("div").withText("Holiday").visible)
.ok()
.expect(Selector("div").withText("Animals").exists)
.notOk();
});
test.meta("testID", "albums-004").meta({ type: "smoke" })(
"Add/Remove Photos to/from album",
async (t) => {
await menu.openPage("albums");
await toolbar.search("Holiday");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await album.openAlbumWithUid(AlbumUid);
const PhotoCount = await photo.getPhotoCount("all");
await menu.openPage("browse");
await toolbar.search("photo:true");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
const SecondPhotoUid = await photo.getNthPhotoUid("image", 1);
await photo.selectPhotoFromUID(SecondPhotoUid);
await photoviewer.openPhotoViewer("uid", FirstPhotoUid);
await photoviewer.triggerPhotoViewerAction("select");
await photoviewer.triggerPhotoViewerAction("close");
await contextmenu.triggerContextMenuAction("album", "Holiday");
await menu.openPage("albums");
await album.openAlbumWithUid(AlbumUid);
const PhotoCountAfterAdd = await photo.getPhotoCount("all");
await t.expect(PhotoCountAfterAdd).eql(PhotoCount + 2);
await photo.selectPhotoFromUID(FirstPhotoUid);
await photo.selectPhotoFromUID(SecondPhotoUid);
await contextmenu.triggerContextMenuAction("remove", "");
const PhotoCountAfterRemove = await photo.getPhotoCount("all");
await t.expect(PhotoCountAfterRemove).eql(PhotoCountAfterAdd - 2);
}
);
test.meta("testID", "albums-005")("Use album search and filters", async (t) => {
await menu.openPage("albums");
if (t.browser.platform === "mobile") {
await toolbar.search("category:Family");
} else {
await toolbar.setFilter("category", "Family");
}
await t.expect(page.cardTitle.nth(0).innerText).contains("Christmas");
const AlbumCount = await album.getAlbumCount("all");
await t.expect(AlbumCount).eql(1);
if (t.browser.platform === "mobile") {
} else {
await toolbar.setFilter("category", "All Categories");
}
await toolbar.search("Holiday");
await t.expect(page.cardTitle.nth(0).innerText).contains("Holiday");
const AlbumCount2 = await album.getAlbumCount("all");
await t.expect(AlbumCount2).eql(1);
});
test.meta("testID", "albums-006")("Test album autocomplete", async (t) => {
await toolbar.search("photo:true");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
await photo.selectPhotoFromUID(FirstPhotoUid);
await contextmenu.openContextMenu();
await t.click(Selector("button.action-album")).click(Selector(".input-album input"));
await t
.expect(page.selectOption.withText("Holiday").visible)
.ok()
.expect(page.selectOption.withText("Christmas").visible)
.ok();
await t.typeText(Selector(".input-album input"), "C", { replace: true });
await t
.expect(page.selectOption.withText("Holiday").visible)
.notOk()
.expect(page.selectOption.withText("Christmas").visible)
.ok()
.expect(page.selectOption.withText("C").visible)
.ok();
});
test.meta("testID", "albums-007").meta({ type: "smoke" })(
"Create, Edit, delete sharing link",
async (t) => {
await page.testCreateEditDeleteSharingLink("albums");
}
);

View file

@ -0,0 +1,146 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import Menu from "../page-model/menu";
import Album from "../page-model/album";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Photo from "../page-model/photo";
import Page from "../page-model/page";
import AlbumDialog from "../page-model/dialog-album";
fixture`Test calendar`.page`${testcafeconfig.url}`;
const menu = new Menu();
const album = new Album();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photo = new Photo();
const page = new Page();
const albumdialog = new AlbumDialog();
test.meta("testID", "calendar-001").meta({ type: "smoke" })("View calendar", async (t) => {
await menu.openPage("calendar");
await t
.expect(Selector("a").withText("May 2019").visible)
.ok()
.expect(Selector("a").withText("October 2019").visible)
.ok();
});
test.meta("testID", "calendar-002")("Update calendar details", async (t) => {
await menu.openPage("calendar");
await toolbar.search("March 2014");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await t.expect(page.cardTitle.nth(0).innerText).contains("March 2014");
await t.click(page.cardTitle.nth(0)).typeText(albumdialog.location, "Snow", { replace: true });
await t.expect(albumdialog.description.value).eql("").expect(albumdialog.category.value).eql("");
await t
.typeText(albumdialog.description, "We went to ski")
.typeText(albumdialog.category, "Mountains")
.pressKey("enter")
.click(albumdialog.dialogSave);
await t
.expect(page.cardTitle.nth(0).innerText)
.contains("March 2014")
.expect(page.cardDescription.nth(0).innerText)
.contains("We went to ski")
.expect(Selector("div.caption").nth(1).innerText)
.contains("Mountains")
.expect(Selector("div.caption").nth(2).innerText)
.contains("Snow");
await album.openNthAlbum(0);
await t.expect(toolbar.toolbarTitle.innerText).contains("March 2014");
await t.expect(toolbar.toolbarDescription.innerText).contains("We went to ski");
await menu.openPage("calendar");
if (t.browser.platform === "mobile") {
await toolbar.search("category:Mountains");
} else {
await toolbar.setFilter("category", "Mountains");
}
await t.expect(page.cardTitle.nth(0).innerText).contains("March 2014");
await album.openAlbumWithUid(AlbumUid);
await toolbar.triggerToolbarAction("edit");
await t
.expect(albumdialog.description.value)
.eql("We went to ski")
.expect(albumdialog.category.value)
.eql("Mountains")
.expect(albumdialog.location.value)
.eql("Snow");
await t
.click(albumdialog.category)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.description)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.location)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.dialogSave);
await menu.openPage("calendar");
await toolbar.search("March 2014");
await t
.expect(page.cardDescription.innerText)
.notContains("We went to ski")
.expect(Selector("div.caption").nth(0).innerText)
.notContains("Snow");
});
test.meta("testID", "calendar-003")("Create, Edit, delete sharing link for calendar", async (t) => {
await page.testCreateEditDeleteSharingLink("calendar");
});
test.meta("testID", "calendar-004").meta({ type: "smoke" })(
"Create/delete album-clone from calendar",
async (t) => {
await menu.openPage("albums");
const AlbumCount = await album.getAlbumCount("all");
await menu.openPage("calendar");
const SecondCalendarUid = await album.getNthAlbumUid("all", 1);
await album.openAlbumWithUid(SecondCalendarUid);
const PhotoCountInCalendar = await photo.getPhotoCount("all");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
const SecondPhotoUid = await photo.getNthPhotoUid("image", 1);
await menu.openPage("calendar");
await album.selectAlbumFromUID(SecondCalendarUid);
await contextmenu.triggerContextMenuAction("clone", "NotYetExistingAlbumForCalendar");
await menu.openPage("albums");
const AlbumCountAfterCreation = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterCreation).eql(AlbumCount + 1);
await toolbar.search("NotYetExistingAlbumForCalendar");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await album.openAlbumWithUid(AlbumUid);
const PhotoCountInAlbum = await photo.getPhotoCount("all");
await t.expect(PhotoCountInAlbum).eql(PhotoCountInCalendar);
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
await menu.openPage("albums");
await album.selectAlbumFromUID(AlbumUid);
await contextmenu.triggerContextMenuAction("delete", "");
await menu.openPage("albums");
const AlbumCountAfterDelete = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterDelete).eql(AlbumCount);
await menu.openPage("calendar");
await album.openAlbumWithUid(SecondCalendarUid);
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
}
);

View file

@ -0,0 +1,61 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import Toolbar from "../page-model/toolbar";
fixture`Test components`.page`${testcafeconfig.url}`;
const toolbar = new Toolbar();
test.meta("testID", "components-001").meta({ type: "smoke" })("Test filter options", async (t) => {
await t.expect(Selector("body").withText("object Object").exists).notOk();
});
test.meta("testID", "components-002").meta({ type: "smoke" })("Fullscreen mode", async (t) => {
await t.click(Selector("div.v-image__image").nth(0));
if (await Selector("#photo-viewer").visible) {
await t
.expect(Selector("#photo-viewer").visible)
.ok()
.expect(Selector("img.pswp__img").visible)
.ok();
} else {
await t.expect(Selector("div.video-viewer").visible).ok();
}
});
test.meta("testID", "components-003").meta({ type: "smoke" })("Mosaic view", async (t) => {
await toolbar.setFilter("view", "Mosaic");
await t
.expect(Selector("div.v-image__image").visible)
.ok()
.expect(Selector("div.p-photo-mosaic").visible)
.ok()
.expect(Selector("div.is-photo div.caption").exists)
.notOk()
.expect(Selector("#photo-viewer").visible)
.notOk();
});
test.meta("testID", "components-004")("List view", async (t) => {
await toolbar.setFilter("view", "List");
await t
.expect(Selector("table.v-datatable").visible)
.ok()
.expect(Selector("div.list-view").visible)
.ok();
});
test.meta("testID", "components-005").meta({ type: "smoke" })("Card view", async (t) => {
await toolbar.setFilter("view", "Cards");
await t
.expect(Selector("div.v-image__image").visible)
.ok()
.expect(Selector("div.is-photo div.caption").visible)
.ok()
.expect(Selector("#photo-viewer").visible)
.notOk();
});

View file

@ -0,0 +1,160 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import Menu from "../page-model/menu";
import Album from "../page-model/album";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Photo from "../page-model/photo";
import Page from "../page-model/page";
import AlbumDialog from "../page-model/dialog-album";
fixture`Test folders`.page`${testcafeconfig.url}`;
const menu = new Menu();
const album = new Album();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photo = new Photo();
const page = new Page();
const albumdialog = new AlbumDialog();
test.meta("testID", "folders-001").meta({ type: "smoke" })("View folders", async (t) => {
await menu.openPage("folders");
await t
.expect(Selector("a").withText("BotanicalGarden").visible)
.ok()
.expect(Selector("a").withText("Kanada").visible)
.ok()
.expect(Selector("a").withText("KorsikaAdventure").visible)
.ok();
});
test.meta("testID", "folders-002")("Update folder details", async (t) => {
await menu.openPage("folders");
await toolbar.search("Kanada");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await t.expect(page.cardTitle.nth(0).innerText).contains("Kanada");
await t.click(page.cardTitle.nth(0));
await t
.expect(albumdialog.title.value)
.eql("Kanada")
.expect(albumdialog.location.value)
.eql("")
.expect(albumdialog.description.value)
.eql("")
.expect(albumdialog.category.value)
.eql("");
await t
.typeText(albumdialog.title, "MyFolder", { replace: true })
.typeText(albumdialog.location, "USA", { replace: true })
.typeText(albumdialog.description, "Last holiday")
.typeText(albumdialog.category, "Mountains")
.pressKey("enter")
.click(albumdialog.dialogSave);
await t
.expect(page.cardTitle.nth(0).innerText)
.contains("MyFolder")
.expect(page.cardDescription.nth(0).innerText)
.contains("Last holiday")
.expect(Selector("div.caption").nth(1).innerText)
.contains("Mountains")
.expect(Selector("div.caption").nth(2).innerText)
.contains("USA");
await album.openNthAlbum(0);
await t
.expect(toolbar.toolbarDescription.nth(0).innerText)
.contains("Last holiday")
.expect(toolbar.toolbarTitle.nth(0).innerText)
.contains("MyFolder");
await menu.openPage("folders");
if (t.browser.platform === "mobile") {
await toolbar.search("category:Mountains");
} else {
await toolbar.setFilter("category", "Mountains");
}
await t.expect(page.cardTitle.nth(0).innerText).contains("MyFolder");
await album.openAlbumWithUid(AlbumUid);
await toolbar.triggerToolbarAction("edit");
await t
.expect(albumdialog.description.value)
.eql("Last holiday")
.expect(albumdialog.category.value)
.eql("Mountains")
.expect(albumdialog.location.value)
.eql("USA");
await t
.typeText(albumdialog.title, "Kanada", { replace: true })
.click(albumdialog.category)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.description)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.location)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.dialogSave);
await menu.openPage("folders");
await toolbar.search("Kanada");
await t
.expect(page.cardTitle.nth(0).innerText)
.contains("Kanada")
.expect(page.cardDescription.nth(0).innerText)
.notContains("We went to ski")
.expect(Selector("div.caption").nth(0).innerText)
.notContains("USA");
});
test.meta("testID", "folders-003")("Create, Edit, delete sharing link", async (t) => {
await page.testCreateEditDeleteSharingLink("folders");
});
test.meta("testID", "folders-004")("Create/delete album-clone from folder", async (t) => {
await menu.openPage("albums");
const AlbumCount = await album.getAlbumCount("all");
await menu.openPage("folders");
const ThirdFolderUid = await album.getNthAlbumUid("all", 2);
await album.openAlbumWithUid(ThirdFolderUid);
const PhotoCountInFolder = await photo.getPhotoCount("all");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
await menu.openPage("folders");
await album.selectAlbumFromUID(ThirdFolderUid);
await contextmenu.triggerContextMenuAction("clone", "NotYetExistingAlbumForFolder");
await menu.openPage("albums");
const AlbumCountAfterCreation = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterCreation).eql(AlbumCount + 1);
await toolbar.search("NotYetExistingAlbumForFolder");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await album.openAlbumWithUid(AlbumUid);
const PhotoCountInAlbum = await photo.getPhotoCount("all");
await t.expect(PhotoCountInAlbum).eql(PhotoCountInFolder);
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await menu.openPage("albums");
await album.selectAlbumFromUID(AlbumUid);
await contextmenu.triggerContextMenuAction("delete", "");
await menu.openPage("albums");
const AlbumCountAfterDelete = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterDelete).eql(AlbumCount);
await menu.openPage("folders");
await album.openAlbumWithUid(ThirdFolderUid);
await photo.checkPhotoVisibility(FirstPhotoUid, true);
});

View file

@ -0,0 +1,209 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import Menu from "../page-model/menu";
import Album from "../page-model/album";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Photo from "../page-model/photo";
import Page from "../page-model/page";
import Label from "../page-model/label";
import PhotoEdit from "../page-model/photo-edit";
fixture`Test labels`.page`${testcafeconfig.url}`;
const menu = new Menu();
const album = new Album();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photo = new Photo();
const page = new Page();
const label = new Label();
const photoedit = new PhotoEdit();
test.meta("testID", "labels-001").meta({ type: "smoke" })(
"Remove/Activate Add/Delete Label from photo",
async (t) => {
await menu.openPage("labels");
await toolbar.search("beacon");
const LabelBeaconUid = await label.getNthLabeltUid(0);
await label.openLabelWithUid(LabelBeaconUid);
await toolbar.setFilter("view", "Cards");
const PhotoBeaconUid = await photo.getNthPhotoUid("all", 0);
await t.click(page.cardTitle.withAttribute("data-uid", PhotoBeaconUid));
const PhotoKeywords = await photoedit.keywords.value;
await t.expect(PhotoKeywords).contains("beacon");
await t
.click(photoedit.labelsTab)
.click(photoedit.removeLabel)
.typeText(photoedit.inputLabelName, "Test")
.click(Selector(photoedit.addLabel))
.click(photoedit.detailsTab);
const PhotoKeywordsAfterEdit = await photoedit.keywords.value;
await t
.expect(PhotoKeywordsAfterEdit)
.contains("test")
.expect(PhotoKeywordsAfterEdit)
.notContains("beacon");
await t.click(photoedit.dialogClose);
await menu.openPage("labels");
await toolbar.search("beacon");
await t.expect(Selector("div.no-results").visible).ok();
await toolbar.search("test");
const LabelTest = await label.getNthLabeltUid(0);
await label.openLabelWithUid(LabelTest);
await t
.click(page.cardTitle.withAttribute("data-uid", PhotoBeaconUid))
.click(photoedit.labelsTab)
.click(photoedit.deleteLabel)
.click(photoedit.activateLabel)
.click(photoedit.detailsTab);
const PhotoKeywordsAfterUndo = await photoedit.keywords.value;
await t
.expect(PhotoKeywordsAfterUndo)
.contains("beacon")
.expect(PhotoKeywordsAfterUndo)
.notContains("test");
await t.click(photoedit.dialogClose);
await menu.openPage("labels");
await toolbar.search("test");
await t.expect(Selector("div.no-results").visible).ok();
await toolbar.search("beacon");
await album.checkAlbumVisibility(LabelBeaconUid, true);
}
);
test.meta("testID", "labels-002")("Toggle between important and all labels", async (t) => {
await menu.openPage("labels");
const ImportantLabelsCount = await label.getLabelCount();
await toolbar.triggerToolbarAction("show-all");
const AllLabelsCount = await label.getLabelCount();
await t.expect(AllLabelsCount).gt(ImportantLabelsCount);
await toolbar.triggerToolbarAction("show-important");
const ImportantLabelsCount2 = await label.getLabelCount();
await t.expect(ImportantLabelsCount).eql(ImportantLabelsCount2);
});
test.meta("testID", "labels-003")("Rename Label", async (t) => {
await menu.openPage("labels");
await toolbar.search("zebra");
const LabelZebraUid = await label.getNthLabeltUid(0);
await label.openNthLabel(0);
const FirstPhotoZebraUid = await photo.getNthPhotoUid("all", 0);
await toolbar.setFilter("view", "Cards");
await t.click(page.cardTitle.withAttribute("data-uid", FirstPhotoZebraUid));
const FirstPhotoTitle = await photoedit.title.value;
const FirstPhotoKeywords = await photoedit.keywords.value;
await t.expect(FirstPhotoTitle).contains("Zebra").expect(FirstPhotoKeywords).contains("zebra");
await t
.click(photoedit.labelsTab)
.click(photoedit.openInlineEdit)
.typeText(photoedit.inputLabelRename, "Horse", { replace: true })
.pressKey("enter")
.click(photoedit.detailsTab);
const FirstPhotoTitleAfterEdit = await photoedit.title.value;
const FirstPhotoKeywordsAfterEdit = await photoedit.keywords.value;
await t
.expect(FirstPhotoTitleAfterEdit)
.contains("Horse")
.expect(FirstPhotoKeywordsAfterEdit)
.contains("horse")
.expect(FirstPhotoTitleAfterEdit)
.notContains("Zebra");
await t.click(photoedit.dialogClose);
await menu.openPage("labels");
await toolbar.search("horse");
await album.checkAlbumVisibility(LabelZebraUid, true);
await label.openLabelWithUid(LabelZebraUid);
await photo.checkPhotoVisibility(FirstPhotoZebraUid, true);
await t
.click(page.cardTitle.withAttribute("data-uid", FirstPhotoZebraUid))
.click(photoedit.labelsTab)
.click(photoedit.openInlineEdit)
.typeText(photoedit.inputLabelRename, "Zebra", { replace: true })
.pressKey("enter")
.click(photoedit.dialogClose);
await menu.openPage("labels");
await toolbar.search("horse");
await t.expect(Selector("div.no-results").visible).ok();
});
test.meta("testID", "labels-003")("Add label to album", async (t) => {
await menu.openPage("albums");
await toolbar.search("Christmas");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await album.openAlbumWithUid(AlbumUid);
const PhotoCount = await photo.getPhotoCount("all");
await menu.openPage("labels");
await toolbar.search("landscape");
const LabelLandscape = await label.getNthLabeltUid(1);
await label.openLabelWithUid(LabelLandscape);
const FirstPhotoLandscape = await photo.getNthPhotoUid("all", 0);
const SecondPhotoLandscape = await photo.getNthPhotoUid("all", 1);
const ThirdPhotoLandscape = await photo.getNthPhotoUid("all", 2);
const FourthPhotoLandscape = await photo.getNthPhotoUid("all", 3);
const FifthPhotoLandscape = await photo.getNthPhotoUid("all", 4);
const SixthPhotoLandscape = await photo.getNthPhotoUid("all", 5);
await menu.openPage("labels");
await label.triggerHoverAction("uid", LabelLandscape, "select");
await contextmenu.checkContextMenuCount("1");
await contextmenu.triggerContextMenuAction("album", "Christmas");
await menu.openPage("albums");
await album.openAlbumWithUid(AlbumUid);
const PhotoCountAfterAdd = await photo.getPhotoCount("all");
await t.expect(PhotoCountAfterAdd).eql(PhotoCount + 6);
await photo.triggerHoverAction("uid", FirstPhotoLandscape, "select");
await photo.triggerHoverAction("uid", SecondPhotoLandscape, "select");
await photo.triggerHoverAction("uid", ThirdPhotoLandscape, "select");
await photo.triggerHoverAction("uid", FourthPhotoLandscape, "select");
await photo.triggerHoverAction("uid", FifthPhotoLandscape, "select");
await photo.triggerHoverAction("uid", SixthPhotoLandscape, "select");
await contextmenu.triggerContextMenuAction("remove", "");
const PhotoCountAfterDelete = await photo.getPhotoCount("all");
await t.expect(PhotoCountAfterDelete).eql(PhotoCountAfterAdd - 6);
});
test.meta("testID", "labels-004")("Delete label", async (t) => {
await menu.openPage("labels");
await toolbar.search("dome");
const LabelDomeUid = await label.getNthLabeltUid(0);
await label.openLabelWithUid(LabelDomeUid);
const FirstPhotoDomeUid = await photo.getNthPhotoUid("all", 0);
await menu.openPage("labels");
await label.triggerHoverAction("uid", LabelDomeUid, "select");
await contextmenu.checkContextMenuCount("1");
await contextmenu.triggerContextMenuAction("delete", "");
await toolbar.search("dome");
await t.expect(Selector("div.no-results").visible).ok();
await menu.openPage("browse");
await toolbar.setFilter("view", "Cards");
await t
.click(page.cardTitle.withAttribute("data-uid", FirstPhotoDomeUid))
.click(photoedit.labelsTab);
await t.expect(Selector("td").withText("No labels found").visible).ok();
await t.typeText(photoedit.inputLabelName, "Dome").click(photoedit.addLabel);
});

View file

@ -0,0 +1,37 @@
import { Selector } from "testcafe";
import testcafeconfig from "../testcafeconfig";
import Menu from "../../page-model/menu";
import Toolbar from "../../page-model/toolbar";
import Page from "../../page-model/page";
import Library from "../../page-model/library";
fixture`Import file from folder`.page`${testcafeconfig.url}`;
const menu = new Menu();
const toolbar = new Toolbar();
const page = new Page();
const library = new Library();
test.meta("testID", "library-import-001").meta({ type: "smoke" })(
"Import files from folder using copy",
async (t) => {
await menu.openPage("labels");
await toolbar.search("bakery");
await t.expect(Selector("div.no-results").visible).ok();
await menu.openPage("library");
await t
.click(library.importTab)
.typeText(library.openImportFolderSelect, "/B", { replace: true })
.click(page.selectOption.nth(0))
.click(library.import)
//TODO replace wait
.wait(60000);
await menu.openPage("labels");
await toolbar.triggerToolbarAction("reload");
await toolbar.search("bakery");
await t.expect(Selector(".is-label").visible).ok();
}
);

View file

@ -0,0 +1,132 @@
import { Selector } from "testcafe";
import testcafeconfig from "../testcafeconfig";
import Menu from "../../page-model/menu";
import Toolbar from "../../page-model/toolbar";
import Photo from "../../page-model/photo";
import Page from "../../page-model/page";
import Library from "../../page-model/library";
import Album from "../../page-model/album";
fixture`Test index`.page`${testcafeconfig.url}`;
const menu = new Menu();
const toolbar = new Toolbar();
const photo = new Photo();
const page = new Page();
const library = new Library();
const album = new Album();
test.meta("testID", "library-index-001").meta({ type: "smoke" })(
"Index files from folder",
async (t) => {
await menu.openPage("labels");
await toolbar.search("cheetah");
await t.expect(Selector("div.no-results").visible).ok();
await menu.openPage("moments");
const MomentCount = await album.getAlbumCount("all");
await menu.openPage("calendar");
if (t.browser.platform === "mobile") {
await t.navigateTo("/calendar?q=December%202013");
} else {
await toolbar.search("December 2013");
}
await t.expect(Selector("div.no-results").visible).ok();
await menu.openPage("folders");
if (t.browser.platform === "mobile") {
await t.navigateTo("/folders?q=moment");
} else {
await toolbar.search("Moment");
}
await t.expect(Selector("div.no-results").visible).ok();
await menu.openPage("states");
if (t.browser.platform === "mobile") {
console.log(t.browser.platform);
await t.navigateTo("/states?q=KwaZulu");
} else {
await toolbar.search("KwaZulu");
}
await t.expect(Selector("div.no-results").visible).ok();
await menu.openPage("originals");
await t.click(Selector(".is-folder").withText("moment"));
await t.expect(Selector("div.no-results").visible).ok();
await menu.openPage("monochrome");
const MonochromeCount = await photo.getPhotoCount("all");
await menu.openPage("library");
await t
.click(library.indexTab)
.click(library.indexFolderSelect)
.click(page.selectOption.withText("/moment"))
.click(library.index)
//TODO replace wait
.wait(50000);
await t.expect(Selector("span").withText("Done.").visible, { timeout: 60000 }).ok();
await menu.openPage("labels");
await toolbar.triggerToolbarAction("reload");
await toolbar.search("cheetah");
await t.expect(Selector(".is-label").visible).ok();
await menu.openPage("moments");
const MomentCountAfterIndex = await album.getAlbumCount("all");
await t
.expect(MomentCountAfterIndex)
.gt(MomentCount)
.click(Selector("a").withText("South Africa 2013"))
.expect(Selector(".is-photo").visible)
.ok();
await menu.openPage("calendar");
await toolbar.triggerToolbarAction("reload");
if (t.browser.platform === "mobile") {
console.log(t.browser.platform);
await t.navigateTo("/calendar?q=December%202013");
} else {
await toolbar.search("December 2013");
}
await t.expect(Selector(".is-album").visible).ok();
await menu.openPage("folders");
await toolbar.triggerToolbarAction("reload");
if (t.browser.platform === "mobile") {
console.log(t.browser.platform);
await t.navigateTo("/folders?q=moment");
} else {
await toolbar.search("Moment");
}
await t.expect(Selector(".is-album", { timeout: 15000 }).visible).ok();
await menu.openPage("states");
if (t.browser.platform === "mobile") {
console.log(t.browser.platform);
await t.navigateTo("/states?q=KwaZulu");
} else {
await toolbar.search("KwaZulu");
}
await t.expect(Selector(".is-album").visible).ok();
await menu.openPage("originals");
await t.expect(Selector(".is-folder").withText("moment").visible, { timeout: 60000 }).ok();
await menu.openPage("monochrome");
const MonochromeCountAfterIndex = await photo.getPhotoCount("all");
await t.expect(MonochromeCountAfterIndex).gt(MonochromeCount);
}
);

View file

@ -0,0 +1,149 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import Menu from "../page-model/menu";
import Album from "../page-model/album";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Photo from "../page-model/photo";
import Page from "../page-model/page";
import AlbumDialog from "../page-model/dialog-album";
fixture`Test moments`.page`${testcafeconfig.url}`;
const menu = new Menu();
const album = new Album();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photo = new Photo();
const page = new Page();
const albumdialog = new AlbumDialog();
test.meta("testID", "moments-001")("Update moment details", async (t) => {
await menu.openPage("moments");
await toolbar.search("Nature");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await t.expect(page.cardTitle.nth(0).innerText).contains("Nature");
await t.click(page.cardTitle.nth(0));
await t
.expect(albumdialog.title.value)
.eql("Nature & Landscape")
.expect(albumdialog.location.value)
.eql("")
.expect(albumdialog.description.value)
.eql("")
.expect(albumdialog.category.value)
.eql("");
await t
.typeText(albumdialog.title, "Winter", { replace: true })
.typeText(albumdialog.location, "Snow-Land", { replace: true })
.typeText(albumdialog.description, "We went to ski")
.typeText(albumdialog.category, "Mountains")
.pressKey("enter")
.click(albumdialog.dialogSave);
await t
.expect(page.cardTitle.nth(0).innerText)
.contains("Winter")
.expect(page.cardDescription.nth(0).innerText)
.contains("We went to ski")
.expect(Selector("div.caption").nth(1).innerText)
.contains("Mountains")
.expect(Selector("div.caption").nth(2).innerText)
.contains("Snow-Land");
await album.openNthAlbum(0);
await t.expect(toolbar.toolbarTitle.innerText).contains("Winter");
await t.expect(toolbar.toolbarDescription.innerText).contains("We went to ski");
await menu.openPage("moments");
if (t.browser.platform === "mobile") {
await toolbar.search("category:Mountains");
} else {
await toolbar.setFilter("category", "Mountains");
}
await t.expect(page.cardTitle.nth(0).innerText).contains("Winter");
await album.openAlbumWithUid(AlbumUid);
await toolbar.triggerToolbarAction("edit");
await t
.expect(albumdialog.description.value)
.eql("We went to ski")
.expect(albumdialog.category.value)
.eql("Mountains")
.expect(albumdialog.location.value)
.eql("Snow-Land");
await t
.typeText(albumdialog.title, "Nature & Landscape", { replace: true })
.click(albumdialog.category)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.description)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.location)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.dialogSave);
await menu.openPage("moments");
await toolbar.search("Nature");
await t
.expect(page.cardTitle.nth(0).innerText)
.contains("Nature & Landscape")
.expect(page.cardDescription.innerText)
.notContains("We went to ski")
.expect(Selector("div.caption").nth(0).innerText)
.notContains("Snow-Land");
});
test.meta("testID", "moments-002")("Create, Edit, delete sharing link for moment", async (t) => {
await page.testCreateEditDeleteSharingLink("moments");
});
test.meta("testID", "moments-003")("Create/delete album-clone from moment", async (t) => {
await menu.openPage("albums");
const AlbumCount = await album.getAlbumCount("all");
await menu.openPage("moments");
const FirstMomentUid = await album.getNthAlbumUid("all", 0);
await album.openAlbumWithUid(FirstMomentUid);
const PhotoCountInMoment = await photo.getPhotoCount("all");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
const SecondPhotoUid = await photo.getNthPhotoUid("image", 1);
await menu.openPage("moments");
await album.selectAlbumFromUID(FirstMomentUid);
await contextmenu.triggerContextMenuAction("clone", "NotYetExistingAlbumForMoment");
await menu.openPage("albums");
const AlbumCountAfterCreation = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterCreation).eql(AlbumCount + 1);
await toolbar.search("NotYetExistingAlbumForMoment");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await album.openAlbumWithUid(AlbumUid);
const PhotoCountInAlbum = await photo.getPhotoCount("all");
await t.expect(PhotoCountInAlbum).eql(PhotoCountInMoment);
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
await menu.openPage("albums");
await album.selectAlbumFromUID(AlbumUid);
await contextmenu.triggerContextMenuAction("delete", "");
await menu.openPage("albums");
const AlbumCountAfterDelete = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterDelete).eql(AlbumCount);
await menu.openPage("moments");
await album.openAlbumWithUid(FirstMomentUid);
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
});

View file

@ -0,0 +1,99 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import Menu from "../page-model/menu";
import Photo from "../page-model/photo";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Album from "../page-model/album";
import Originals from "../page-model/originals";
fixture`Test files`.page`${testcafeconfig.url}`;
const menu = new Menu();
const photo = new Photo();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const album = new Album();
const originals = new Originals();
test.meta("testID", "originals-001").meta({ type: "smoke" })("Navigate in originals", async (t) => {
await menu.openPage("originals");
await t.click(Selector("button").withText("Vacation"));
const FirstItemInVacationName = await Selector("div.result", { timeout: 15000 }).nth(0).innerText;
const KanadaFolderUid = await originals.getNthFolderUid(0);
const SecondItemInVacationName = await Selector("div.result").nth(1).innerText;
await t
.expect(FirstItemInVacationName)
.contains("Kanada")
.expect(SecondItemInVacationName)
.contains("Korsika");
await originals.openFolderWithUid(KanadaFolderUid);
const FirstItemInKanadaName = await Selector("div.result").nth(0).innerText;
const SecondItemInKanadaName = await Selector("div.result").nth(1).innerText;
await t
.expect(FirstItemInKanadaName)
.contains("BotanicalGarden")
.expect(SecondItemInKanadaName)
.contains("originals-001_2.jpg");
await t.click(Selector("button").withText("BotanicalGarden"));
const FirstItemInBotanicalGardenName = await Selector("div.result", { timeout: 15000 }).nth(0)
.innerText;
await t.expect(FirstItemInBotanicalGardenName).contains("originals-001_1.jpg");
await t.click(Selector('a[href="/library/files/Vacation"]'));
const FolderCount = await originals.getFolderCount();
await t.expect(FolderCount).eql(2);
});
test.meta("testID", "originals-002").meta({ type: "smoke" })(
"Add original files to album",
async (t) => {
await menu.openPage("albums");
await toolbar.search("KanadaVacation");
await t.expect(Selector("div.no-results").visible).ok();
await menu.openPage("originals");
await t.click(Selector("button").withText("Vacation"));
const KanadaFolderUid = await originals.getNthFolderUid(0);
await originals.openFolderWithUid(KanadaFolderUid);
const FilesCountInKanada = await originals.getFileCount();
await t.click(Selector("button").withText("BotanicalGarden"));
const FilesCountInKanadaSubfolder = await originals.getFileCount();
await t.navigateTo("/library/files/Vacation");
await originals.triggerHoverAction("is-folder", "uid", KanadaFolderUid, "select");
await contextmenu.checkContextMenuCount("1");
await contextmenu.triggerContextMenuAction("album", "KanadaVacation");
await menu.openPage("albums");
await toolbar.search("KanadaVacation");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await album.openAlbumWithUid(AlbumUid);
const PhotoCountAfterAdd = await photo.getPhotoCount("all");
await t.expect(PhotoCountAfterAdd).eql(FilesCountInKanada + FilesCountInKanadaSubfolder);
await menu.openPage("albums");
await album.triggerHoverAction("uid", AlbumUid, "select");
await contextmenu.checkContextMenuCount("1");
await contextmenu.triggerContextMenuAction("delete", "");
}
);
test.meta("testID", "originals-003")("Download available in originals", async (t) => {
await menu.openPage("originals");
const FirstFile = await originals.getNthFileUid(0);
await originals.triggerHoverAction("is-file", "uid", FirstFile, "select");
await contextmenu.checkContextMenuCount("1");
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.clearSelection();
const FirstFolder = await originals.getNthFolderUid(0);
await originals.triggerHoverAction("is-folder", "uid", FirstFolder, "select");
await contextmenu.checkContextMenuCount("1");
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.clearSelection();
});

View file

@ -0,0 +1,280 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import Menu from "../page-model/menu";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Photo from "../page-model/photo";
import Subject from "../page-model/subject";
import PhotoEdit from "../page-model/photo-edit";
fixture`Test people`.page`${testcafeconfig.url}`;
const menu = new Menu();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photo = new Photo();
const subject = new Subject();
const photoedit = new PhotoEdit();
test.meta("testID", "people-001").meta({ type: "smoke" })(
"Add name to new face and rename subject",
async (t) => {
await menu.openPage("people");
await t.click(subject.newTab);
await subject.triggerToolbarAction("reload", "");
const FaceCount = await subject.getFaceCount();
await t.click(subject.recognizedTab);
const SubjectCount = await subject.getSubjectCount();
await t.click(subject.newTab);
const FirstFaceID = await subject.getNthFaceUid(0);
await subject.openFaceWithUid(FirstFaceID);
const PhotosInFaceCount = await photo.getPhotoCount("all");
await menu.openPage("people");
await t.click(subject.newTab);
await subject.addNameToFace(FirstFaceID, "Jane Doe");
await subject.triggerToolbarAction("reload");
const FaceCountAfterAdd = await subject.getFaceCount();
await t.expect(FaceCountAfterAdd).eql(FaceCount - 1);
await t.click(subject.recognizedTab);
await subject.checkFaceVisibility(FirstFaceID, false);
await t.eval(() => location.reload());
await t.wait(6000);
const SubjectCountAfterAdd = await subject.getSubjectCount();
await t.expect(SubjectCountAfterAdd).eql(SubjectCount + 1);
await toolbar.search("Jane");
const JaneUID = await subject.getNthSubjectUid(0);
await t
.expect(Selector("a[data-uid=" + JaneUID + "] div.caption").innerText)
.contains(PhotosInFaceCount.toString());
await subject.openSubjectWithUid(JaneUID);
const PhotosInSubjectCount = await photo.getPhotoCount("all");
await t.expect(PhotosInFaceCount).eql(PhotosInSubjectCount);
await photo.triggerHoverAction("nth", 0, "select");
await photo.triggerHoverAction("nth", 1, "select");
await photo.triggerHoverAction("nth", 2, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.peopleTab);
await t.expect(photoedit.inputName.nth(0).value).contains("Jane Doe");
await t.click(photoedit.dialogClose);
await menu.openPage("people");
await subject.renameSubject(JaneUID, "Max Mu");
await t
.expect(Selector("a[data-uid=" + JaneUID + "] div.v-card__title").innerText)
.contains("Max Mu");
await subject.openSubjectWithUid(JaneUID);
await t.eval(() => location.reload());
await contextmenu.checkContextMenuCount("3");
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.peopleTab);
await t.expect(photoedit.inputName.nth(0).value).contains("Max Mu");
await t.click(photoedit.dialogNext);
await t.expect(photoedit.inputName.nth(0).value).contains("Max Mu").click(photoedit.dialogNext);
await t
.expect(photoedit.inputName.nth(0).value)
.contains("Max Mu")
.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await toolbar.search("person:max-mu");
const PhotosInSubjectAfterRenameCount = await photo.getPhotoCount("all");
await t.expect(PhotosInSubjectAfterRenameCount).eql(PhotosInSubjectCount);
}
);
test.meta("testID", "people-002").meta({ type: "smoke" })(
"Add + Reject name on people tab",
async (t) => {
await menu.openPage("people");
await t.click(subject.newTab);
await subject.triggerToolbarAction("reload");
const FirstFaceID = await subject.getNthFaceUid(0);
await subject.addNameToFace(FirstFaceID, "Andrea Doe");
await t.click(subject.recognizedTab);
await toolbar.search("Andrea");
const AndreaUID = await subject.getNthSubjectUid(0);
await subject.openSubjectWithUid(AndreaUID);
await t.eval(() => location.reload());
await t.wait(5000);
const PhotosInAndreaCount = await photo.getPhotoCount("all");
await photo.triggerHoverAction("nth", 1, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await t
.click(photoedit.peopleTab)
.expect(photoedit.inputName.nth(0).value)
.eql("Andrea Doe")
.click(photoedit.rejectName.nth(0));
await t.expect(photoedit.inputName.nth(0).value).eql("");
await t
.typeText(photoedit.inputName.nth(0), "Nicole", { replace: true })
.pressKey("enter")
.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await t.eval(() => location.reload());
await t.wait(5000);
const PhotosInAndreaAfterRejectCount = await photo.getPhotoCount("all");
const Diff = PhotosInAndreaCount - PhotosInAndreaAfterRejectCount;
await toolbar.search("person:nicole");
await t.eval(() => location.reload());
await t.wait(5000);
const PhotosInNicoleCount = await photo.getPhotoCount("all");
await t.expect(Diff).gte(PhotosInNicoleCount);
}
);
test.meta("testID", "people-003")("Test mark subject as favorite", async (t) => {
await menu.openPage("people");
const FirstSubjectUid = await subject.getNthSubjectUid(0);
const SecondSubjectUid = await subject.getNthSubjectUid(1);
await subject.triggerHoverAction("uid", SecondSubjectUid, "favorite");
await subject.triggerToolbarAction("reload");
const FirstSubjectUidAfterFavorite = await subject.getNthSubjectUid(0);
await t.expect(FirstSubjectUid).notEql(FirstSubjectUidAfterFavorite);
await t.expect(SecondSubjectUid).eql(FirstSubjectUidAfterFavorite);
await subject.checkHoverActionState("uid", SecondSubjectUid, "favorite", true);
await subject.triggerHoverAction("uid", SecondSubjectUid, "favorite");
await subject.checkHoverActionState("uid", SecondSubjectUid, "favorite", false);
});
test.meta("testID", "people-004")("Test new face autocomplete", async (t) => {
await menu.openPage("people");
await t.click(subject.newTab);
await subject.triggerToolbarAction("reload");
const FirstFaceID = await subject.getNthFaceUid(0);
await t
.expect(Selector("div.menuable__content__active").nth(0).visible)
.notOk()
.click(Selector("div[data-id=" + FirstFaceID + "] div.input-name input"))
.typeText(Selector("div[data-id=" + FirstFaceID + "] div.input-name input"), "Otto");
await t
.expect(Selector("div.menuable__content__active").nth(0).withText("Otto Visible").visible)
.ok();
});
test.meta("testID", "people-005")("Remove face", async (t) => {
await toolbar.search("face:new");
const FirstPhotoUid = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("nth", 0, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.peopleTab);
const MarkerCount = await subject.getMarkerCount();
if ((await photoedit.inputName.nth(0).value) == "") {
await t
.expect(photoedit.undoRemoveMarker.nth(0).visible)
.notOk()
.expect(photoedit.inputName.nth(0).value)
.eql("")
.click(photoedit.removeMarker)
.expect(photoedit.undoRemoveMarker.nth(0).visible)
.ok()
.click(photoedit.undoRemoveMarker);
} else if ((await photoedit.inputName.nth(0).value) != "") {
await t
.expect(photoedit.inputName.nth(1).value)
.eql("")
.click(photoedit.removeMarker)
.expect(photoedit.undoRemoveMarker.nth(0).visible)
.ok()
.click(photoedit.undoRemoveMarker);
}
await t.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await t.eval(() => location.reload());
await t.wait(5000);
await photo.triggerHoverAction("uid", FirstPhotoUid, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.peopleTab);
if ((await photoedit.inputName.nth(0).value) == "") {
await t
.expect(photoedit.undoRemoveMarker.nth(0).visible)
.notOk()
.expect(photoedit.inputName.nth(0).value)
.eql("")
.click(photoedit.removeMarker)
.expect(photoedit.undoRemoveMarker.nth(0).visible)
.ok();
} else if ((await photoedit.inputName.nth(0).value) != "") {
await t
.expect(photoedit.undoRemoveMarker.nth(0).visible)
.notOk()
.expect(photoedit.inputName.nth(1).value)
.eql("")
.click(photoedit.removeMarker)
.expect(photoedit.undoRemoveMarker.nth(0).visible)
.ok();
}
await t.click(photoedit.dialogClose);
await t.eval(() => location.reload());
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.peopleTab);
const MarkerCountAfterRemove = await subject.getMarkerCount();
await t.expect(MarkerCountAfterRemove).eql(MarkerCount - 1);
});
test.meta("testID", "people-006")("Hide face", async (t) => {
await menu.openPage("people");
await t.click(subject.newTab);
await subject.triggerToolbarAction("reload");
const FirstFaceID = await subject.getNthFaceUid(0);
await subject.checkFaceVisibility(FirstFaceID, true);
await subject.triggerHoverAction("id", FirstFaceID, "hidden");
await t.eval(() => location.reload());
await t.wait(5000);
await subject.checkFaceVisibility(FirstFaceID, false);
await subject.triggerToolbarAction("show-hidden");
await t.eval(() => location.reload());
await t.wait(6000);
await subject.checkFaceVisibility(FirstFaceID, true);
await subject.triggerHoverAction("id", FirstFaceID, "hidden");
await subject.triggerToolbarAction("exclude-hidden");
await t.eval(() => location.reload());
await t.wait(6000);
await subject.checkFaceVisibility(FirstFaceID, true);
});
test.meta("testID", "people-007")("Hide person", async (t) => {
await menu.openPage("people");
await t.click(subject.recognizedTab);
const FirstPersonUid = await subject.getNthSubjectUid(0);
await subject.checkSubjectVisibility("uid", FirstPersonUid, true);
await subject.triggerHoverAction("uid", FirstPersonUid, "hidden");
await t.eval(() => location.reload());
await t.wait(6000);
await subject.checkSubjectVisibility("uid", FirstPersonUid, false);
await subject.triggerToolbarAction("show-hidden");
await t.eval(() => location.reload());
await t.wait(6000);
await subject.checkSubjectVisibility("uid", FirstPersonUid, true);
await subject.triggerHoverAction("uid", FirstPersonUid, "hidden");
await subject.triggerToolbarAction("exclude-hidden");
await t.eval(() => location.reload());
await t.wait(5000);
await subject.checkSubjectVisibility("uid", FirstPersonUid, true);
});

View file

@ -0,0 +1,494 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import Menu from "../page-model/menu";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Photo from "../page-model/photo";
import PhotoEdit from "../page-model/photo-edit";
import Album from "../page-model/album";
import Subject from "../page-model/subject";
import Label from "../page-model/label";
fixture`Test photos archive and private functionalities`.page`${testcafeconfig.url}`;
const menu = new Menu();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photo = new Photo();
const photoedit = new PhotoEdit();
const album = new Album();
const label = new Label();
const subject = new Subject();
test.meta("testID", "photos-archive-private-001").meta({ type: "smoke" })(
"Private/unprivate photo/video using clipboard and list",
async (t) => {
await toolbar.setFilter("view", "Mosaic");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
const SecondPhotoUid = await photo.getNthPhotoUid("image", 1);
const ThirdPhotoUid = await photo.getNthPhotoUid("image", 2);
const FirstVideoUid = await photo.getNthPhotoUid("video", 0);
const SecondVideoUid = await photo.getNthPhotoUid("video", 1);
const ThirdVideoUid = await photo.getNthPhotoUid("video", 2);
await menu.openPage("private");
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(SecondPhotoUid, false);
await photo.checkPhotoVisibility(ThirdPhotoUid, false);
await photo.checkPhotoVisibility(FirstVideoUid, false);
await photo.checkPhotoVisibility(SecondVideoUid, false);
await photo.checkPhotoVisibility(ThirdVideoUid, false);
await menu.openPage("browse");
await photo.triggerHoverAction("uid", FirstPhotoUid, "select");
await photo.triggerHoverAction("uid", FirstVideoUid, "select");
await contextmenu.triggerContextMenuAction("private", "");
await toolbar.setFilter("view", "List");
await photo.triggerListViewActions("uid", SecondPhotoUid, "private");
await photo.triggerListViewActions("uid", SecondVideoUid, "private");
await toolbar.setFilter("view", "Cards");
await photo.triggerHoverAction("uid", ThirdPhotoUid, "select");
await photo.triggerHoverAction("uid", ThirdVideoUid, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await photoedit.turnSwitchOn("private");
await t.click(photoedit.dialogNext);
await photoedit.turnSwitchOn("private");
await t.click(photoedit.dialogClose);
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(SecondPhotoUid, false);
await photo.checkPhotoVisibility(ThirdPhotoUid, false);
await photo.checkPhotoVisibility(FirstVideoUid, false);
await photo.checkPhotoVisibility(SecondVideoUid, false);
await photo.checkPhotoVisibility(ThirdVideoUid, false);
await menu.openPage("video");
await photo.checkPhotoVisibility(FirstVideoUid, false);
await photo.checkPhotoVisibility(SecondVideoUid, false);
await photo.checkPhotoVisibility(ThirdVideoUid, false);
await menu.openPage("private");
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
await photo.checkPhotoVisibility(ThirdPhotoUid, true);
await photo.checkPhotoVisibility(FirstVideoUid, true);
await photo.checkPhotoVisibility(SecondVideoUid, true);
await photo.checkPhotoVisibility(ThirdVideoUid, true);
await contextmenu.clearSelection();
await photo.triggerHoverAction("uid", FirstPhotoUid, "select");
await photo.triggerHoverAction("uid", SecondPhotoUid, "select");
await photo.triggerHoverAction("uid", ThirdPhotoUid, "select");
await photo.triggerHoverAction("uid", FirstVideoUid, "select");
await photo.triggerHoverAction("uid", SecondVideoUid, "select");
await photo.triggerHoverAction("uid", ThirdVideoUid, "select");
await contextmenu.checkContextMenuCount("6");
await contextmenu.triggerContextMenuAction("private", "");
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(SecondPhotoUid, false);
await photo.checkPhotoVisibility(ThirdPhotoUid, false);
await photo.checkPhotoVisibility(FirstVideoUid, false);
await photo.checkPhotoVisibility(SecondVideoUid, false);
await photo.checkPhotoVisibility(ThirdVideoUid, false);
await menu.openPage("browse");
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
await photo.checkPhotoVisibility(ThirdPhotoUid, true);
await photo.checkPhotoVisibility(FirstVideoUid, true);
await photo.checkPhotoVisibility(SecondVideoUid, true);
await photo.checkPhotoVisibility(ThirdVideoUid, true);
}
);
test.meta("testID", "photos-archive-private-002").meta({ type: "smoke" })(
"Archive/restore video, photos, private photos and review photos using clipboard",
async (t) => {
await toolbar.setFilter("view", "Mosaic");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
const SecondPhotoUid = await photo.getNthPhotoUid("image", 1);
const FirstVideoUid = await photo.getNthPhotoUid("video", 0);
await menu.openPage("private");
const FirstPrivatePhotoUid = await photo.getNthPhotoUid("all", 0);
await menu.openPage("review");
const FirstReviewPhotoUid = await photo.getNthPhotoUid("all", 0);
await menu.openPage("archive");
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(SecondPhotoUid, false);
await photo.checkPhotoVisibility(FirstVideoUid, false);
await photo.checkPhotoVisibility(FirstPrivatePhotoUid, false);
await photo.checkPhotoVisibility(FirstReviewPhotoUid, false);
await menu.openPage("browse");
await photo.triggerHoverAction("uid", FirstPhotoUid, "select");
await photo.triggerHoverAction("uid", SecondPhotoUid, "select");
await photo.triggerHoverAction("uid", FirstVideoUid, "select");
await contextmenu.triggerContextMenuAction("archive", "");
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(SecondPhotoUid, false);
await photo.checkPhotoVisibility(FirstVideoUid, false);
await photo.checkPhotoVisibility(FirstPrivatePhotoUid, false);
await photo.checkPhotoVisibility(FirstReviewPhotoUid, false);
await menu.openPage("review");
await photo.triggerHoverAction("uid", FirstReviewPhotoUid, "select");
await contextmenu.triggerContextMenuAction("archive", "");
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
await photo.checkPhotoVisibility(FirstReviewPhotoUid, false);
await menu.openPage("private");
await photo.triggerHoverAction("uid", FirstPrivatePhotoUid, "select");
await contextmenu.triggerContextMenuAction("archive", "");
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
await photo.checkPhotoVisibility(FirstPrivatePhotoUid, false);
await menu.openPage("archive");
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
await photo.checkPhotoVisibility(FirstVideoUid, true);
await photo.checkPhotoVisibility(FirstPrivatePhotoUid, true);
await photo.checkPhotoVisibility(FirstReviewPhotoUid, true);
await photo.triggerHoverAction("uid", FirstPrivatePhotoUid, "select");
await photo.triggerHoverAction("uid", FirstReviewPhotoUid, "select");
await photo.triggerHoverAction("uid", FirstPhotoUid, "select");
await photo.triggerHoverAction("uid", SecondPhotoUid, "select");
await photo.triggerHoverAction("uid", FirstVideoUid, "select");
await contextmenu.triggerContextMenuAction("restore", "");
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(SecondPhotoUid, false);
await photo.checkPhotoVisibility(FirstVideoUid, false);
await photo.checkPhotoVisibility(FirstPrivatePhotoUid, false);
await photo.checkPhotoVisibility(FirstReviewPhotoUid, false);
await menu.openPage("browse");
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
await photo.checkPhotoVisibility(FirstVideoUid, true);
await photo.checkPhotoVisibility(FirstPrivatePhotoUid, false);
await photo.checkPhotoVisibility(FirstReviewPhotoUid, false);
await menu.openPage("private");
await photo.checkPhotoVisibility(FirstPrivatePhotoUid, true);
await menu.openPage("review");
await photo.checkPhotoVisibility(FirstReviewPhotoUid, true);
}
);
test.meta("testID", "photos-archive-private-003")(
"Check that archived files are not shown in monochrome/panoramas/stacks/scans/review/albums/favorites/private/videos/calendar/moments/states/labels/folders/originals",
async (t) => {
await menu.openPage("archive");
const InitialPhotoCountInArchive = await photo.getPhotoCount("all");
await menu.openPage("monochrome");
const MonochromePhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", MonochromePhoto, "select");
await menu.openPage("panoramas");
const PanoramaPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", PanoramaPhoto, "select");
await menu.openPage("stacks");
const StackedPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", StackedPhoto, "select");
await menu.openPage("scans");
const ScannedPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", ScannedPhoto, "select");
await menu.openPage("review");
const ReviewPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", ReviewPhoto, "select");
await menu.openPage("favorites");
const FavoritesPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", FavoritesPhoto, "select");
await menu.openPage("private");
const PrivatePhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", PrivatePhoto, "select");
await menu.openPage("video");
const Video = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", Video, "select");
await menu.openPage("calendar");
await toolbar.search("January 2017");
await album.openNthAlbum(0);
const CalendarPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", CalendarPhoto, "select");
await menu.openPage("moments");
await album.openNthAlbum(0);
const MomentPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", MomentPhoto, "select");
await menu.openPage("states");
await toolbar.search("Western Cape");
await album.openNthAlbum(0);
const StatesPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", StatesPhoto, "select");
await menu.openPage("labels");
await toolbar.search("Seashore");
await label.openNthLabel(0);
const LabelPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", LabelPhoto, "select");
await menu.openPage("people");
await subject.openNthSubject(1);
const SubjectPhoto = await photo.getNthPhotoUid("all", 1);
await photo.triggerHoverAction("uid", SubjectPhoto, "select");
await menu.openPage("folders");
await toolbar.search("archive");
await album.openNthAlbum(0);
const FolderPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", FolderPhoto, "select");
await contextmenu.checkContextMenuCount("14");
await contextmenu.triggerContextMenuAction("archive", "");
await menu.openPage("archive");
await toolbar.triggerToolbarAction("reload");
const PhotoCountInArchiveAfterArchive = await photo.getPhotoCount("all");
await t.expect(PhotoCountInArchiveAfterArchive).eql(InitialPhotoCountInArchive + 14);
await menu.openPage("monochrome");
await photo.checkPhotoVisibility(MonochromePhoto, false);
await menu.openPage("panoramas");
await photo.checkPhotoVisibility(PanoramaPhoto, false);
await menu.openPage("stacks");
await photo.checkPhotoVisibility(StackedPhoto, false);
await menu.openPage("scans");
await photo.checkPhotoVisibility(ScannedPhoto, false);
await menu.openPage("review");
await photo.checkPhotoVisibility(ReviewPhoto, false);
await menu.openPage("favorites");
await photo.checkPhotoVisibility(FavoritesPhoto, false);
await menu.openPage("private");
await photo.checkPhotoVisibility(PrivatePhoto, false);
await menu.openPage("video");
await photo.checkPhotoVisibility(Video, false);
await t.navigateTo("/calendar/aqmxlr71p6zo22dk/january-2017");
await photo.checkPhotoVisibility(CalendarPhoto, false);
await menu.openPage("moments");
await album.openNthAlbum(0);
await photo.checkPhotoVisibility(MomentPhoto, false);
await t.navigateTo("/states/aqmxlr71tebcohrw/western-cape-south-africa");
await photo.checkPhotoVisibility(StatesPhoto, false);
await t.navigateTo("/all?q=label%3Aseashore");
await photo.checkPhotoVisibility(LabelPhoto, false);
await menu.openPage("people");
await subject.openNthSubject(1);
await photo.checkPhotoVisibility(SubjectPhoto, false);
await t.navigateTo("/folders/aqnah1321mgkt1w2/archive");
await photo.checkPhotoVisibility(FolderPhoto, false);
await menu.openPage("archive");
await photo.triggerHoverAction("uid", MonochromePhoto, "select");
await photo.triggerHoverAction("uid", PanoramaPhoto, "select");
await photo.triggerHoverAction("uid", StackedPhoto, "select");
await photo.triggerHoverAction("uid", ScannedPhoto, "select");
await photo.triggerHoverAction("uid", FavoritesPhoto, "select");
await photo.triggerHoverAction("uid", ReviewPhoto, "select");
await photo.triggerHoverAction("uid", PrivatePhoto, "select");
await photo.triggerHoverAction("uid", Video, "select");
await photo.triggerHoverAction("uid", CalendarPhoto, "select");
await photo.triggerHoverAction("uid", MomentPhoto, "select");
await photo.triggerHoverAction("uid", StatesPhoto, "select");
await photo.triggerHoverAction("uid", LabelPhoto, "select");
await photo.triggerHoverAction("uid", SubjectPhoto, "select");
await photo.triggerHoverAction("uid", FolderPhoto, "select");
await contextmenu.checkContextMenuCount("14");
await contextmenu.triggerContextMenuAction("restore", "");
const PhotoCountInArchiveAfterRestore = await photo.getPhotoCount("all");
await t.expect(PhotoCountInArchiveAfterRestore).eql(InitialPhotoCountInArchive);
await menu.openPage("private");
await photo.checkPhotoVisibility(PrivatePhoto, true);
}
);
test.meta("testID", "photos-archive-private-004").meta({ type: "smoke" })(
"Check that private files are not shown in monochrome/panoramas/stacks/scans/review/albums/favorites/archive/videos/calendar/moments/states/labels/folders/originals",
async (t) => {
await menu.openPage("private");
const InitialPhotoCountInPrivate = await photo.getPhotoCount("all");
await menu.openPage("monochrome");
const MonochromePhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", MonochromePhoto, "select");
await menu.openPage("panoramas");
const PanoramaPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", PanoramaPhoto, "select");
await menu.openPage("stacks");
const StackedPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", StackedPhoto, "select");
await menu.openPage("scans");
const ScannedPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", ScannedPhoto, "select");
await menu.openPage("review");
const ReviewPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", ReviewPhoto, "select");
await menu.openPage("favorites");
const FavoritesPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", FavoritesPhoto, "select");
await menu.openPage("video");
const Video = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", Video, "select");
await menu.openPage("albums");
await toolbar.search("Holiday");
await album.openNthAlbum(0);
const AlbumPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", AlbumPhoto, "select");
await menu.openPage("calendar");
await toolbar.search("January 2017");
await album.openNthAlbum(0);
const CalendarPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", CalendarPhoto, "select");
await menu.openPage("moments");
await album.openNthAlbum(0);
const MomentPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", MomentPhoto, "select");
await menu.openPage("states");
await toolbar.search("Western Cape");
await album.openNthAlbum(0);
const StatesPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", StatesPhoto, "select");
await menu.openPage("labels");
await toolbar.search("Seashore");
await label.openNthLabel(0);
const LabelPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", LabelPhoto, "select");
await menu.openPage("people");
await subject.openNthSubject(1);
const SubjectPhoto = await photo.getNthPhotoUid("all", 1);
await photo.triggerHoverAction("uid", SubjectPhoto, "select");
await menu.openPage("folders");
await toolbar.search("archive");
await album.openNthAlbum(0);
const FolderPhoto = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", FolderPhoto, "select");
await contextmenu.checkContextMenuCount("14");
await contextmenu.triggerContextMenuAction("private", "");
await menu.openPage("private");
await toolbar.triggerToolbarAction("reload");
const PhotoCountInPrivateAfterArchive = await photo.getPhotoCount("all");
await t.expect(PhotoCountInPrivateAfterArchive).eql(InitialPhotoCountInPrivate + 14);
await menu.openPage("monochrome");
await photo.checkPhotoVisibility(MonochromePhoto, false);
await menu.openPage("panoramas");
await photo.checkPhotoVisibility(PanoramaPhoto, false);
await menu.openPage("stacks");
await photo.checkPhotoVisibility(StackedPhoto, false);
await menu.openPage("scans");
await photo.checkPhotoVisibility(ScannedPhoto, false);
await menu.openPage("review");
await photo.checkPhotoVisibility(ReviewPhoto, false);
await menu.openPage("favorites");
await photo.checkPhotoVisibility(FavoritesPhoto, false);
await menu.openPage("video");
await photo.checkPhotoVisibility(Video, false);
await t.navigateTo("/albums?q=Holiday");
await album.openNthAlbum(0);
await photo.checkPhotoVisibility(AlbumPhoto, true);
await t.navigateTo("/calendar/aqmxlr71p6zo22dk/january-2017");
await photo.checkPhotoVisibility(CalendarPhoto, false);
await menu.openPage("moments");
await album.openNthAlbum(0);
await photo.checkPhotoVisibility(MomentPhoto, false);
await t.navigateTo("/states/aqmxlr71tebcohrw/western-cape-south-africa");
await photo.checkPhotoVisibility(StatesPhoto, false);
await t.navigateTo("/all?q=label%3Aseashore");
await photo.checkPhotoVisibility(LabelPhoto, false);
await menu.openPage("people");
await subject.openNthSubject(1);
await photo.checkPhotoVisibility(SubjectPhoto, false);
await t.navigateTo("/folders/aqnah1321mgkt1w2/archive");
await photo.checkPhotoVisibility(FolderPhoto, false);
await menu.openPage("private");
await photo.triggerHoverAction("uid", MonochromePhoto, "select");
await photo.triggerHoverAction("uid", PanoramaPhoto, "select");
await photo.triggerHoverAction("uid", StackedPhoto, "select");
await photo.triggerHoverAction("uid", ScannedPhoto, "select");
await photo.triggerHoverAction("uid", FavoritesPhoto, "select");
await photo.triggerHoverAction("uid", ReviewPhoto, "select");
await photo.triggerHoverAction("uid", Video, "select");
await photo.triggerHoverAction("uid", CalendarPhoto, "select");
await photo.triggerHoverAction("uid", AlbumPhoto, "select");
await photo.triggerHoverAction("uid", MomentPhoto, "select");
await photo.triggerHoverAction("uid", StatesPhoto, "select");
await photo.triggerHoverAction("uid", LabelPhoto, "select");
await photo.triggerHoverAction("uid", SubjectPhoto, "select");
await photo.triggerHoverAction("uid", FolderPhoto, "select");
await contextmenu.checkContextMenuCount("14");
await contextmenu.triggerContextMenuAction("private", "");
await toolbar.triggerToolbarAction("reload");
const PhotoCountInPrivateAfterRestore = await photo.getPhotoCount("all");
await t.expect(PhotoCountInPrivateAfterRestore).eql(InitialPhotoCountInPrivate);
await menu.openPage("monochrome");
await photo.checkPhotoVisibility(MonochromePhoto, true);
await menu.openPage("panoramas");
await photo.checkPhotoVisibility(PanoramaPhoto, true);
await menu.openPage("stacks");
await photo.checkPhotoVisibility(StackedPhoto, true);
await menu.openPage("scans");
await photo.checkPhotoVisibility(ScannedPhoto, true);
await menu.openPage("review");
await photo.checkPhotoVisibility(ReviewPhoto, true);
await menu.openPage("favorites");
await photo.checkPhotoVisibility(FavoritesPhoto, true);
await menu.openPage("video");
await photo.checkPhotoVisibility(Video, true);
await t.navigateTo("/albums?q=Holiday");
await album.openNthAlbum(0);
await photo.checkPhotoVisibility(AlbumPhoto, true);
await t.navigateTo("/calendar/aqmxlr71p6zo22dk/january-2017");
await photo.checkPhotoVisibility(CalendarPhoto, true);
await menu.openPage("moments");
await album.openNthAlbum(0);
await photo.checkPhotoVisibility(MomentPhoto, true);
await t.navigateTo("/states/aqmxlr71tebcohrw/western-cape-south-africa");
await photo.checkPhotoVisibility(StatesPhoto, true);
await t.navigateTo("/all?q=label%3Aseashore");
await photo.checkPhotoVisibility(LabelPhoto, true);
await menu.openPage("people");
await subject.openNthSubject(1);
await photo.checkPhotoVisibility(SubjectPhoto, true);
await t.navigateTo("/folders/aqnah1321mgkt1w2/archive");
await photo.checkPhotoVisibility(FolderPhoto, true);
}
);

View file

@ -0,0 +1,119 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import { RequestLogger } from "testcafe";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Photo from "../page-model/photo";
import PhotoViewer from "../page-model/photoviewer";
import Page from "../page-model/page";
const logger = RequestLogger(/http:\/\/localhost:2343\/api\/v1\/*/, {
logResponseHeaders: true,
logResponseBody: true,
});
fixture`Test photos download`.page`${testcafeconfig.url}`
.requestHooks(logger)
.skip("Does not work in container and we have no content-disposition header anymore");
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photo = new Photo();
const photoviewer = new PhotoViewer();
const page = new Page();
test.meta("testID", "photos-download-001").meta({ type: "smoke" })(
"Test download jpg file from context menu and fullscreen",
async (t) => {
await toolbar.search("name:monochrome-2.jpg");
const PhotoUid = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", PhotoUid, "select");
await logger.clear();
await contextmenu.triggerContextMenuAction("download", "");
const requestInfo = await logger.requests[1].response;
console.log(requestInfo);
const requestInfo0 = await logger.requests[0].response;
console.log(requestInfo0);
await page.validateDownloadRequest(requestInfo, "monochrome-2", ".jpg");
await logger.clear();
await contextmenu.clearSelection();
await toolbar.search("name:IMG_20200711_174006.jpg");
const SecondPhotoUid = await photo.getNthPhotoUid("all", 0);
await t.click(Selector("div").withAttribute("data-uid", SecondPhotoUid));
await photoviewer.openPhotoViewer("uid", SecondPhotoUid);
await logger.clear();
await photoviewer.triggerPhotoViewerAction("download");
await logger.clear();
await photoviewer.triggerPhotoViewerAction("close");
}
);
test.meta("testID", "photos-download-002").meta({ type: "smoke" })(
"Test download video from context menu",
async (t) => {
await toolbar.search("name:Mohn.mp4");
const PhotoUid = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", PhotoUid, "select");
await logger.clear();
await contextmenu.triggerContextMenuAction("download", "");
const requestInfo = await logger.requests[0].response;
console.log(requestInfo);
const requestInfo2 = await logger.requests[1].response;
await page.validateDownloadRequest(requestInfo, "Mohn", ".mp4.jpg");
await page.validateDownloadRequest(requestInfo2, "Mohn", ".mp4");
await logger.clear();
await contextmenu.clearSelection();
}
);
test.meta("testID", "photos-download-003")(
"Test download multiple jpg files from context menu",
async (t) => {
await toolbar.search("name:panorama_2.jpg");
const PhotoUid = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", PhotoUid, "select");
await toolbar.search("name:IMG_6478.JPG");
const SecondPhotoUid = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", SecondPhotoUid, "select");
await logger.clear();
await contextmenu.triggerContextMenuAction("download", "");
const requestInfo = await logger.requests[1].response;
console.log(requestInfo);
await page.validateDownloadRequest(requestInfo, "photoprism-download", ".zip");
await logger.clear();
await contextmenu.clearSelection();
}
);
//TODO Check RAW files as well
test.meta("testID", "photos-download-004")(
"Test raw file from context menu and fullscreen mode",
async (t) => {
await toolbar.search("name:elephantRAW");
const PhotoUid = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", PhotoUid, "select");
await logger.clear();
await contextmenu.triggerContextMenuAction("download", "");
const requestInfo = await logger.requests[1].response;
await page.validateDownloadRequest(requestInfo, "elephantRAW", ".JPG");
await logger.clear();
await contextmenu.clearSelection();
await t.click(Selector("div").withAttribute("data-uid", Photo));
await t.expect(Selector("#photo-viewer").visible).ok().hover(Selector(".action-download"));
await logger.clear();
await t.click(Selector(".action-download"));
const requestInfo3 = await logger.requests[1].response;
//const requestInfo4 = await logger.requests[2].response;
await page.validateDownloadRequest(requestInfo3, "elephantRAW", ".JPG");
//await page.validateDownloadRequest(requestInfo4, "elephantRAW", ".mp4");
await logger.clear();
}
);

View file

@ -0,0 +1,266 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import fs from "fs";
import Menu from "../page-model/menu";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Photo from "../page-model/photo";
import Page from "../page-model/page";
import PhotoEdit from "../page-model/photo-edit";
import Originals from "../page-model/originals";
import Album from "../page-model/album";
import Library from "../page-model/library";
fixture`Test photos upload and delete`.page`${testcafeconfig.url}`;
const menu = new Menu();
const album = new Album();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photo = new Photo();
const page = new Page();
const photoedit = new PhotoEdit();
const originals = new Originals();
const library = new Library();
test.meta("testID", "photos-upload-delete-001").meta({ type: "smoke" })(
"Upload + Delete jpg/json",
async (t) => {
await t.expect(fs.existsSync("../storage/acceptance/originals/2020/10")).notOk();
await toolbar.search("digikam");
const PhotoCount = await photo.getPhotoCount("all");
await t.expect(PhotoCount).eql(0);
await toolbar.triggerToolbarAction("upload");
await t
.setFilesToUpload(Selector(".input-upload"), [
"./upload-files/digikam.jpg",
"./upload-files/digikam.json",
])
.wait(15000);
const PhotoCountAfterUpload = await photo.getPhotoCount("all");
await t.expect(PhotoCountAfterUpload).eql(1);
const UploadedPhoto = await photo.getNthPhotoUid("all", 0);
await t.navigateTo("/library/files/2020/10");
const FileCount = await originals.getFileCount();
await t.expect(FileCount).eql(2);
await menu.openPage("browse");
await toolbar.search("digikam");
await photo.triggerHoverAction("uid", UploadedPhoto, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.filesTab);
await t
.expect(Selector("div.caption").withText(".json").visible)
.ok()
.expect(Selector("div.caption").withText(".jpg").visible)
.ok();
await t.click(photoedit.dialogClose);
if (t.browser.platform !== "mobile") {
await t.expect(fs.existsSync("../storage/acceptance/originals/2020/10")).ok();
const originalsLength = fs.readdirSync("../storage/acceptance/originals/2020/10").length;
await t.expect(originalsLength).eql(2);
}
await contextmenu.triggerContextMenuAction("archive", "");
await menu.openPage("archive");
await photo.triggerHoverAction("uid", UploadedPhoto, "select");
await contextmenu.triggerContextMenuAction("delete", "");
await menu.openPage("browse");
await toolbar.search("digikam");
await photo.checkPhotoVisibility(UploadedPhoto, false);
await t.navigateTo("/library/files/2020/10");
const FileCountAfterDelete = await originals.getFileCount();
await t.expect(FileCountAfterDelete).eql(0);
if (t.browser.platform !== "mobile") {
const originalsLengthAfterDelete = fs.readdirSync(
"../storage/acceptance/originals/2020/10"
).length;
await t.expect(originalsLengthAfterDelete).eql(0);
}
}
);
test.meta("testID", "photos-upload-delete-002")("Upload + Delete video", async (t) => {
await t.expect(fs.existsSync("../storage/acceptance/originals/2020/06")).notOk();
await toolbar.search("korn");
const PhotoCount = await photo.getPhotoCount("all");
await t.expect(PhotoCount).eql(0);
await toolbar.triggerToolbarAction("upload");
await t.setFilesToUpload(Selector(".input-upload"), ["./upload-files/korn.mp4"]).wait(15000);
const PhotoCountAfterUpload = await photo.getPhotoCount("all");
await t.expect(PhotoCountAfterUpload).eql(1);
const UploadedPhoto = await photo.getNthPhotoUid("all", 0);
await t.navigateTo("/library/files/2020/06");
const FileCount = await originals.getFileCount();
await t.expect(FileCount).eql(1);
await menu.openPage("browse");
await toolbar.search("korn");
await photo.triggerHoverAction("uid", UploadedPhoto, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.filesTab);
await t
.expect(Selector("div.caption").withText(".mp4").visible)
.ok()
.expect(Selector("div.caption").withText(".jpg").visible)
.ok();
await t.click(photoedit.dialogClose);
if (t.browser.platform !== "mobile") {
await t.expect(fs.existsSync("../storage/acceptance/originals/2020/06")).ok();
const originalsLength = fs.readdirSync("../storage/acceptance/originals/2020/06").length;
await t.expect(originalsLength).eql(1);
const sidecarLength = fs.readdirSync("../storage/acceptance/originals/2020/06").length;
await t.expect(sidecarLength).eql(1);
}
await contextmenu.triggerContextMenuAction("archive", "");
await menu.openPage("archive");
await photo.triggerHoverAction("uid", UploadedPhoto, "select");
await contextmenu.triggerContextMenuAction("delete", "");
await menu.openPage("browse");
await toolbar.search("korn");
await photo.checkPhotoVisibility(UploadedPhoto, false);
await t.navigateTo("/library/files/2020/06");
const FileCountAfterDelete = await originals.getFileCount();
await t.expect(FileCountAfterDelete).eql(0);
if (t.browser.platform !== "mobile") {
const originalsLengthAfterDelete = fs.readdirSync(
"../storage/acceptance/originals/2020/06"
).length;
await t.expect(originalsLengthAfterDelete).eql(0);
const sidecarLengthAfterDelete = fs.readdirSync(
"../storage/acceptance/originals/2020/06"
).length;
await t.expect(sidecarLengthAfterDelete).eql(0);
}
});
test.meta("testID", "photos-upload-delete-003")("Upload to existing Album + Delete", async (t) => {
await menu.openPage("albums");
await toolbar.search("Christmas");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await album.openAlbumWithUid(AlbumUid);
const PhotoCount = await photo.getPhotoCount("all");
await toolbar.triggerToolbarAction("upload");
await t
.click(Selector(".input-albums"))
.click(page.selectOption.withText("Christmas"))
.setFilesToUpload(Selector(".input-upload"), ["./upload-files/ladybug.jpg"])
.wait(15000);
const PhotoCountAfterUpload = await photo.getPhotoCount("all");
await t.expect(PhotoCountAfterUpload).eql(PhotoCount + 1);
await menu.openPage("browse");
await toolbar.search("ladybug");
const UploadedPhotoUid = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", UploadedPhotoUid, "select");
await contextmenu.triggerContextMenuAction("archive", "");
await menu.openPage("archive");
await photo.triggerHoverAction("uid", UploadedPhotoUid, "select");
await contextmenu.triggerContextMenuAction("delete", "");
await menu.openPage("browse");
await toolbar.search("ladybug");
await photo.checkPhotoVisibility(UploadedPhotoUid, false);
await menu.openPage("albums");
await album.openAlbumWithUid(AlbumUid);
await photo.checkPhotoVisibility(UploadedPhotoUid, false);
const PhotoCountAfterDelete = await photo.getPhotoCount("all");
await t.expect(PhotoCountAfterDelete).eql(PhotoCount);
});
test.meta("testID", "photos-upload-delete-004")("Upload jpg to new Album + Delete", async (t) => {
await menu.openPage("albums");
const AlbumCount = await album.getAlbumCount("all");
await toolbar.triggerToolbarAction("upload");
await t
.click(Selector(".input-albums"))
.typeText(Selector(".input-albums input"), "NewCreatedAlbum")
.pressKey("enter")
.setFilesToUpload(Selector(".input-upload"), ["./upload-files/digikam.jpg"])
.wait(15000);
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
const AlbumCountAfterUpload = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterUpload).eql(AlbumCount + 1);
await toolbar.search("NewCreatedAlbum");
await album.openNthAlbum(0);
const PhotoCount = await photo.getPhotoCount("all");
await t.expect(PhotoCount).eql(1);
await menu.openPage("browse");
await toolbar.search("digikam");
const UploadedPhotoUid = await photo.getNthPhotoUid("all", 0);
await photo.triggerHoverAction("uid", UploadedPhotoUid, "select");
await contextmenu.triggerContextMenuAction("archive", "");
await menu.openPage("archive");
await photo.triggerHoverAction("uid", UploadedPhotoUid, "select");
await contextmenu.triggerContextMenuAction("delete", "");
await menu.openPage("browse");
await toolbar.search("digikam");
await photo.checkPhotoVisibility(UploadedPhotoUid, false);
await menu.openPage("albums");
await toolbar.search("NewCreatedAlbum");
await album.openNthAlbum(0);
await photo.checkPhotoVisibility(UploadedPhotoUid, false);
const PhotoCountAfterDelete = await photo.getPhotoCount("all");
await t.expect(PhotoCountAfterDelete).eql(0);
await menu.openPage("albums");
await toolbar.search("NewCreatedAlbum");
await album.triggerHoverAction("nth", 0, "select");
await contextmenu.checkContextMenuCount("1");
await contextmenu.triggerContextMenuAction("delete", "");
});
test.meta("testID", "photos-upload-delete-005").meta({ type: "smoke" })(
"Try uploading nsfw file",
async (t) => {
await toolbar.triggerToolbarAction("upload");
await t
.setFilesToUpload(Selector(".input-upload"), ["./upload-files/hentai_2.jpg"])
.wait(15000);
await menu.openPage("library");
await t.click(library.logsTab);
await t.expect(Selector("p").withText("hentai_2.jpg might be offensive").visible).ok();
}
);
test.meta("testID", "photos-upload-delete-006").meta({ type: "smoke" })(
"Try uploading txt file",
async (t) => {
await toolbar.triggerToolbarAction("upload");
await t.setFilesToUpload(Selector(".input-upload"), ["./upload-files/foo.txt"]).wait(15000);
await menu.openPage("library");
await t.click(library.logsTab);
await t.expect(Selector("p").withText(" foo.txt is not a jpeg file").visible).ok();
}
);

View file

@ -0,0 +1,377 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import { ClientFunction } from "testcafe";
import Menu from "../page-model/menu";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Photo from "../page-model/photo";
import PhotoViewer from "../page-model/photoviewer";
import Page from "../page-model/page";
import PhotoEdit from "../page-model/photo-edit";
const scroll = ClientFunction((x, y) => window.scrollTo(x, y));
const getcurrentPosition = ClientFunction(() => window.pageYOffset);
fixture`Test photos`.page`${testcafeconfig.url}`;
const menu = new Menu();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photo = new Photo();
const photoviewer = new PhotoViewer();
const page = new Page();
const photoedit = new PhotoEdit();
test.meta("testID", "photos-001")("Scroll to top", async (t) => {
await toolbar.setFilter("view", "Cards");
await t
.expect(Selector("button.is-photo-scroll-top").exists)
.notOk()
.expect(getcurrentPosition())
.eql(0)
.expect(Selector('div[class="v-image__image v-image__image--cover"]').nth(0).visible)
.ok();
await scroll(0, 1400);
await scroll(0, 900);
await t.click(Selector("button.p-scroll-top")).expect(getcurrentPosition()).eql(0);
});
//TODO Covered by admin role test
test.meta("testID", "photos-002")(
"Download single photo/video using clipboard and fullscreen mode",
async (t) => {
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
const SecondPhotoUid = await photo.getNthPhotoUid("image", 1);
const FirstVideoUid = await photo.getNthPhotoUid("video", 0);
await photoviewer.openPhotoViewer("uid", SecondPhotoUid);
await photoviewer.checkPhotoViewerActionAvailability("download", true);
await photoviewer.triggerPhotoViewerAction("close");
await photo.triggerHoverAction("uid", FirstPhotoUid, "select");
await photo.triggerHoverAction("uid", FirstVideoUid, "select");
await contextmenu.checkContextMenuCount("2");
await contextmenu.checkContextMenuActionAvailability("download", true);
}
);
test.meta("testID", "photos-003").meta({ type: "smoke" })(
"Approve photo using approve and by adding location",
async (t) => {
await menu.openPage("review");
const FirstPhotoUid = await photo.getNthPhotoUid("all", 0);
const SecondPhotoUid = await photo.getNthPhotoUid("all", 1);
const ThirdPhotoUid = await photo.getNthPhotoUid("all", 2);
await menu.openPage("browse");
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(SecondPhotoUid, false);
await menu.openPage("review");
await photo.triggerHoverAction("uid", FirstPhotoUid, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.detailsClose);
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.detailsApprove);
if (t.browser.platform === "mobile") {
await t.click(photoedit.detailsApply).click(photoedit.detailsClose);
} else {
await t.click(photoedit.detailsDone);
}
await photo.triggerHoverAction("uid", SecondPhotoUid, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await t
.typeText(photoedit.latitude, "9.999", { replace: true })
.typeText(photoedit.longitude, "9.999", { replace: true });
if (t.browser.platform === "mobile") {
await t.click(photoedit.detailsApply).click(photoedit.detailsClose);
} else {
await t.click(photoedit.detailsDone);
}
await toolbar.setFilter("view", "Cards");
const ApproveButtonThirdPhoto =
'div.is-photo[data-uid="' + ThirdPhotoUid + '"] button.action-approve';
await t.click(Selector(ApproveButtonThirdPhoto));
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(SecondPhotoUid, false);
await photo.checkPhotoVisibility(ThirdPhotoUid, false);
await menu.openPage("browse");
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
await photo.checkPhotoVisibility(ThirdPhotoUid, true);
}
);
test.meta("testID", "photos-004").meta({ type: "smoke" })("Like/dislike photo/video", async (t) => {
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
const SecondPhotoUid = await photo.getNthPhotoUid("image", 1);
const FirstVideoUid = await photo.getNthPhotoUid("video", 0);
await menu.openPage("favorites");
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(SecondPhotoUid, false);
await photo.checkPhotoVisibility(FirstVideoUid, false);
await menu.openPage("browse");
await photo.triggerHoverAction("uid", FirstPhotoUid, "favorite");
await photo.triggerHoverAction("uid", FirstVideoUid, "favorite");
await photo.triggerHoverAction("uid", SecondPhotoUid, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await photoedit.turnSwitchOn("favorite");
await t.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(FirstVideoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
await menu.openPage("favorites");
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(FirstVideoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
await photo.triggerHoverAction("uid", SecondPhotoUid, "favorite");
await photo.triggerHoverAction("uid", FirstVideoUid, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await photoedit.turnSwitchOff("favorite");
await t.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await photoviewer.openPhotoViewer("uid", FirstPhotoUid);
await photoviewer.triggerPhotoViewerAction("like");
await photoviewer.triggerPhotoViewerAction("close");
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(FirstVideoUid, false);
await photo.checkPhotoVisibility(SecondPhotoUid, false);
});
test.meta("testID", "photos-005").meta({ type: "smoke" })("Edit photo/video", async (t) => {
await toolbar.setFilter("view", "Cards");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
await t.click(page.cardTitle.withAttribute("data-uid", FirstPhotoUid));
await t.expect(photoedit.latitude.visible).ok();
await t.click(photoedit.dialogNext);
await t.expect(photoedit.dialogPrevious.getAttribute("disabled")).notEql("disabled");
await t.click(photoedit.dialogPrevious).click(photoedit.dialogClose);
await photoviewer.openPhotoViewer("uid", FirstPhotoUid);
await photoviewer.triggerPhotoViewerAction("edit");
const FirstPhotoTitle = await photoedit.title.value;
const FirstPhotoLocalTime = await photoedit.localTime.value;
const FirstPhotoDay = await photoedit.day.value;
const FirstPhotoMonth = await photoedit.month.value;
const FirstPhotoYear = await photoedit.year.value;
const FirstPhotoTimezone = await photoedit.timezone.value;
const FirstPhotoLatitude = await photoedit.latitude.value;
const FirstPhotoLongitude = await photoedit.longitude.value;
const FirstPhotoAltitude = await photoedit.altitude.value;
const FirstPhotoCountry = await photoedit.country.value;
const FirstPhotoCamera = await photoedit.camera.innerText;
const FirstPhotoIso = await photoedit.iso.value;
const FirstPhotoExposure = await photoedit.exposure.value;
const FirstPhotoLens = await photoedit.lens.innerText;
const FirstPhotoFnumber = await photoedit.fnumber.value;
const FirstPhotoFocalLength = await photoedit.focallength.value;
const FirstPhotoSubject = await photoedit.subject.value;
const FirstPhotoArtist = await photoedit.artist.value;
const FirstPhotoCopyright = await photoedit.copyright.value;
const FirstPhotoLicense = await photoedit.license.value;
const FirstPhotoDescription = await photoedit.description.value;
const FirstPhotoKeywords = await photoedit.keywords.value;
const FirstPhotoNotes = await photoedit.notes.value;
await t
.typeText(photoedit.title, "Not saved photo title", { replace: true })
.click(photoedit.detailsClose)
.click(Selector("button.action-date-edit").withAttribute("data-uid", FirstPhotoUid));
await t.expect(photoedit.title.value).eql(FirstPhotoTitle);
await photoedit.editPhoto(
"New Photo Title",
"Europe/Moscow",
"15",
"07",
"2019",
"04:30:30",
"-1",
"41.15333",
"20.168331",
"32",
"1/32",
"29",
"33",
"Super nice edited photo",
"Happy",
"Happy2020",
"Super nice cat license",
"Description of a nice image :)",
", cat, love",
"Some notes"
);
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
await t
.expect(page.cardTitle.withAttribute("data-uid", FirstPhotoUid).innerText)
.eql("New Photo Title");
await photo.triggerHoverAction("uid", FirstPhotoUid, "select");
await contextmenu.triggerContextMenuAction("edit", "");
//const expectedValues = [{ FirstPhotoTitle: photoedit.title }, { "bluh bla": photoedit.day }];
/*const expectedValues = [
[FirstPhotoTitle, photoedit.title],
["blah", photoedit.day],
];
await photoedit.checkEditFormValuesNewNew(expectedValues);*/
await photoedit.checkEditFormValues(
"New Photo Title",
"15",
"07",
"2019",
"04:30:30",
"Europe/Moscow",
"Albania",
"-1",
"",
"",
"",
"32",
"1/32",
"",
"29",
"33",
"Super nice edited photo",
"Happy",
"Happy2020",
"Super nice cat license",
"Description of a nice image :)",
"cat",
"Some notes"
);
await photoedit.undoPhotoEdit(
FirstPhotoTitle,
FirstPhotoTimezone,
FirstPhotoDay,
FirstPhotoMonth,
FirstPhotoYear,
FirstPhotoLocalTime,
FirstPhotoAltitude,
FirstPhotoLatitude,
FirstPhotoLongitude,
FirstPhotoCountry,
FirstPhotoIso,
FirstPhotoExposure,
FirstPhotoFnumber,
FirstPhotoFocalLength,
FirstPhotoSubject,
FirstPhotoArtist,
FirstPhotoCopyright,
FirstPhotoLicense,
FirstPhotoDescription,
FirstPhotoKeywords,
FirstPhotoNotes
);
await contextmenu.checkContextMenuCount("1");
await contextmenu.clearSelection();
});
test.meta("testID", "photos-006")("Navigate from card view to place", async (t) => {
await toolbar.setFilter("view", "Cards");
await t.click(page.cardLocation.nth(0));
await t
.expect(Selector("#map").exists, { timeout: 15000 })
.ok()
.expect(Selector("div.p-map-control").visible)
.ok()
.expect(Selector(".input-search input").value)
.notEql("");
});
test.meta("testID", "photos-007")("Mark photos/videos as panorama/scan", async (t) => {
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
const FirstVideoUid = await photo.getNthPhotoUid("video", 1);
await menu.openPage("scans");
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(FirstVideoUid, false);
await menu.openPage("panoramas");
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(FirstVideoUid, false);
await menu.openPage("browse");
await photo.triggerHoverAction("uid", FirstPhotoUid, "select");
await photo.triggerHoverAction("uid", FirstVideoUid, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await photoedit.turnSwitchOn("scan");
await photoedit.turnSwitchOn("panorama");
await t.click(photoedit.dialogNext);
await photoedit.turnSwitchOn("scan");
await photoedit.turnSwitchOn("panorama");
await t.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(FirstVideoUid, true);
await menu.openPage("scans");
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(FirstVideoUid, false);
await menu.openPage("panoramas");
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(FirstVideoUid, true);
await photo.triggerHoverAction("uid", FirstPhotoUid, "select");
await photo.triggerHoverAction("uid", FirstVideoUid, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await photoedit.turnSwitchOff("scan");
await photoedit.turnSwitchOff("panorama");
await t.click(photoedit.dialogNext);
await photoedit.turnSwitchOff("scan");
await photoedit.turnSwitchOff("panorama");
await t.click(photoedit.dialogClose);
await toolbar.triggerToolbarAction("reload");
await contextmenu.clearSelection();
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(FirstVideoUid, false);
});

View file

@ -0,0 +1,53 @@
import { Selector } from "testcafe";
import { ClientFunction } from "testcafe";
import testcafeconfig from "./testcafeconfig.json";
import Menu from "../page-model/menu";
const getLocation = ClientFunction(() => document.location.href);
fixture`Search and open photo from places`.page`${testcafeconfig.url}`.skip(
"Places don't loadin chrome from within the container"
);
const menu = new Menu();
test.meta("testID", "places-001")("Test places", async (t) => {
await menu.openPage("places");
await t
.expect(Selector("#map").exists, { timeout: 15000 })
.ok()
.expect(Selector("div.p-map-control").visible)
.ok();
await t.typeText(Selector('input[aria-label="Search"]'), "Berlin").pressKey("enter");
await t
.expect(Selector("div.p-map-control").visible)
.ok()
.expect(getLocation())
.contains("Berlin");
await menu.openPage("browse");
await t.expect(Selector("div.is-photo").exists).ok();
await menu.openPage("places");
await t
.expect(Selector("#map").exists, { timeout: 15000 })
.ok()
.expect(Selector("div.p-map-control").visible)
.ok();
await t
.typeText(Selector('input[aria-label="Search"]'), "canada", { replace: true })
.pressKey("enter")
.wait(8000);
await t.expect(Selector('div[title="Cape / Bowen Island / 2019"]').visible).ok();
await t.click(Selector('div[title="Cape / Bowen Island / 2019"]'));
await t.expect(Selector("#photo-viewer").visible).ok();
});

View file

@ -0,0 +1,37 @@
import { Selector } from "testcafe";
import testcafeconfig from "../testcafeconfig";
import Menu from "../../page-model/menu";
fixture`Test about`.page`${testcafeconfig.url}`;
const menu = new Menu();
test.meta("testID", "about-001")("About page is displayed with all links", async (t) => {
await menu.openPage("about");
await t
.expect(Selector("h2").withText("Trademarks").visible)
.ok()
.expect(Selector('a[href="https://photoprism.app/"]').visible)
.ok()
.expect(Selector('a[href="https://www.patreon.com/photoprism"]').visible)
.ok()
.expect(Selector('a[href="https://github.com/photoprism/photoprism/projects/5"]').visible)
.ok()
.expect(Selector('a[href="https://docs.photoprism.app/"]').visible)
.ok()
.expect(Selector('a[href="/about/license"]').visible)
.ok()
.expect(Selector('a[href="https://gitter.im/browseyourlife/community"]').visible)
.ok()
.expect(Selector('a[href="https://twitter.com/photoprism_app"]').visible)
.ok();
});
test.meta("testID", "about-002")("License page is displayed with all links", async (t) => {
await menu.openPage("license");
await t
.expect(Selector("h3").withText("GNU AFFERO GENERAL PUBLIC LICENSE").visible)
.ok()
.expect(Selector('a[href="https://www.gnu.org/licenses/agpl-3.0.en.html"]').visible)
.ok();
});

View file

@ -0,0 +1,416 @@
import { Selector } from "testcafe";
import testcafeconfig from "../testcafeconfig";
import Menu from "../../page-model/menu";
import Toolbar from "../../page-model/toolbar";
import ContextMenu from "../../page-model/context-menu";
import PhotoViewer from "../../page-model/photoviewer";
import Page from "../../page-model/page";
import Photo from "../../page-model/photo";
import PhotoEdit from "../../page-model/photo-edit";
import Album from "../../page-model/album";
import Settings from "../../page-model/settings";
import Library from "../../page-model/library";
fixture`Test settings`.page`${testcafeconfig.url}`;
const menu = new Menu();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photoviewer = new PhotoViewer();
const page = new Page();
const photo = new Photo();
const photoedit = new PhotoEdit();
const album = new Album();
const settings = new Settings();
const library = new Library();
test.meta("testID", "settings-general-001").meta({ type: "smoke" })("Disable delete", async (t) => {
await menu.openPage("archive");
await photo.triggerHoverAction("nth", 0, "select");
await contextmenu.checkContextMenuActionAvailability("delete", true);
await contextmenu.clearSelection();
await menu.openPage("settings");
await t.click(settings.deleteCheckbox);
await menu.openPage("archive");
await photo.triggerHoverAction("nth", 0, "select");
await contextmenu.checkContextMenuActionAvailability("restore", true);
await contextmenu.checkContextMenuActionAvailability("delete", false);
await contextmenu.clearSelection();
await menu.openPage("browse");
await toolbar.search("stack:true");
await photo.triggerHoverAction("nth", 0, "select");
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.filesTab);
await t.click(photoedit.toggleExpandFile.nth(1));
await t.expect(photoedit.deleteFile.visible).notOk();
await t.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await menu.openPage("settings");
await t.click(settings.deleteCheckbox);
});
test.meta("testID", "settings-general-002").meta({ type: "smoke" })(
"Change language",
async (t) => {
await t.expect(Selector(".nav-browse").innerText).contains("Search");
await menu.openPage("settings");
await t
.click(settings.languageInput)
.hover(Selector("div").withText("Deutsch").parent('div[role="listitem"]'))
.click(Selector("div").withText("Deutsch").parent('div[role="listitem"]'));
await t.eval(() => location.reload());
await t.expect(Selector(".nav-browse").innerText).contains("Suche");
await t
.click(settings.languageInput)
.hover(Selector("div").withText("English").parent('div[role="listitem"]'))
.click(Selector("div").withText("English").parent('div[role="listitem"]'));
await t.expect(Selector(".nav-browse").innerText).contains("Search");
}
);
test.meta("testID", "settings-general-003").meta({ type: "smoke" })(
"Disable pages: import, originals, logs, moments, places, library",
async (t) => {
await t.expect(page.cardLocation.exists).ok();
await menu.openPage("library");
await t
.expect(library.importTab.visible)
.ok()
.expect(library.logsTab.visible)
.ok()
.expect(library.indexTab.visible)
.ok();
await menu.checkMenuItemAvailability("originals", true);
await menu.checkMenuItemAvailability("folders", true);
await menu.checkMenuItemAvailability("moments", true);
await menu.checkMenuItemAvailability("places", true);
await menu.checkMenuItemAvailability("library", true);
await menu.openPage("settings");
await t
.click(settings.importCheckbox)
.click(settings.filesCheckbox)
.click(settings.momentsCheckbox)
.click(settings.logsCheckbox)
.click(settings.placesCheckbox);
await t.eval(() => location.reload());
await menu.openPage("browse");
await t.expect(page.cardLocation.exists).notOk();
await menu.openPage("library");
await t
.expect(library.importTab.visible)
.notOk()
.expect(library.logsTab.visible)
.notOk()
.expect(library.indexTab.visible)
.ok();
await menu.checkMenuItemAvailability("originals", false);
await menu.checkMenuItemAvailability("folders", true);
await menu.checkMenuItemAvailability("moments", false);
await menu.checkMenuItemAvailability("places", false);
await menu.checkMenuItemAvailability("library", true);
await menu.openPage("settings");
await t
.click(settings.importCheckbox)
.click(settings.filesCheckbox)
.click(settings.momentsCheckbox)
.click(settings.logsCheckbox)
.click(settings.placesCheckbox)
.click(settings.libraryCheckbox);
await menu.checkMenuItemAvailability("originals", false);
await menu.checkMenuItemAvailability("folders", true);
await menu.checkMenuItemAvailability("moments", true);
await menu.checkMenuItemAvailability("places", true);
await menu.checkMenuItemAvailability("library", false);
await t.click(settings.libraryCheckbox);
await menu.checkMenuItemAvailability("originals", true);
await menu.checkMenuItemAvailability("library", true);
}
);
test.meta("testID", "settings-general-004").meta({ type: "smoke" })(
"Disable people and labels",
async (t) => {
await t.click(page.cardTitle.nth(0));
await t.click(photoedit.labelsTab);
await t.expect(photoedit.addLabel.visible).ok();
await t.click(photoedit.peopleTab);
await t.expect(Selector("div.p-faces").visible).ok();
await t.click(photoedit.dialogClose);
await menu.checkMenuItemAvailability("people", true);
await menu.checkMenuItemAvailability("labels", true);
await menu.openPage("settings");
await t.click(settings.peopleCheckbox).click(settings.labelsCheckbox);
await t.eval(() => location.reload());
await menu.openPage("browse");
await t.click(page.cardTitle.nth(0));
await t.click(photoedit.labelsTab);
await t.expect(photoedit.addLabel.exists).notOk();
await t.click(photoedit.peopleTab);
await t.expect(Selector("div.p-faces ").exists).notOk();
await t.click(photoedit.dialogClose);
await menu.checkMenuItemAvailability("people", false);
await menu.checkMenuItemAvailability("labels", false);
await menu.openPage("settings");
await t.click(settings.peopleCheckbox).click(settings.labelsCheckbox);
await menu.checkMenuItemAvailability("people", true);
await menu.checkMenuItemAvailability("labels", true);
}
);
test.meta("testID", "settings-general-005").meta({ type: "smoke" })(
"Disable private, archive and quality filter",
async (t) => {
await menu.checkMenuItemAvailability("private", true);
await menu.checkMenuItemAvailability("archive", true);
await menu.checkMenuItemAvailability("review", true);
await toolbar.search("photo:true stack:true");
await photo.triggerHoverAction("nth", 0, "select");
await contextmenu.checkContextMenuActionAvailability("archive", true);
await contextmenu.checkContextMenuActionAvailability("private", true);
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.infoTab);
await photoedit.checkFieldDisabled(photoedit.privateInput, false);
await t.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await toolbar.search("Viewpoint / Mexico / 2017");
await photo.checkPhotoVisibility("pqmxlr7188hz4bih", false);
await toolbar.search("Truck / Vancouver / 2019");
await photo.checkPhotoVisibility("pqmxlr0kg161o9ek", false);
await toolbar.search("Archive / 2020");
await photo.checkPhotoVisibility("pqnah1k2frui6p63", false);
await menu.openPage("settings");
await t
.click(settings.archiveCheckbox)
.click(settings.privateCheckbox)
.click(Selector(settings.libraryTab))
.click(settings.reviewCheckbox);
await t.eval(() => location.reload());
await menu.checkMenuItemAvailability("private", false);
await menu.checkMenuItemAvailability("archive", false);
await menu.checkMenuItemAvailability("review", false);
await menu.openPage("browse");
await toolbar.search("photo:true");
await photo.triggerHoverAction("nth", 0, "select");
await contextmenu.checkContextMenuActionAvailability("archive", false);
await contextmenu.checkContextMenuActionAvailability("private", false);
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.infoTab);
//await photoedit.checkFieldDisabled(photoedit.privateInput, true);
await t.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await toolbar.search("Viewpoint / Mexico / 2017");
await photo.checkPhotoVisibility("pqmxlr7188hz4bih", true);
await toolbar.search("Truck / Vancouver / 2019");
await photo.checkPhotoVisibility("pqmxlr0kg161o9ek", false);
await toolbar.search("Archive / 2020");
await photo.checkPhotoVisibility("pqnah1k2frui6p63", true);
await menu.openPage("settings");
await t
.click(settings.privateCheckbox)
.click(settings.archiveCheckbox)
.click(Selector(settings.libraryTab))
.click(settings.reviewCheckbox);
await menu.checkMenuItemAvailability("archive", true);
await menu.checkMenuItemAvailability("private", true);
await menu.checkMenuItemAvailability("review", true);
}
);
test.meta("testID", "settings-general-006").meta({ type: "smoke" })(
"Disable upload, download, edit and share",
async (t) => {
await toolbar.checkToolbarActionAvailability("upload", true);
await toolbar.search("photo:true stack:true");
await photo.triggerHoverAction("nth", 0, "select");
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("edit", true);
await contextmenu.triggerContextMenuAction("edit", "");
await photoedit.checkAllDetailsFieldsDisabled(false);
await t.expect(photoedit.infoTab.visible).ok();
await t.click(photoedit.filesTab);
await t
.expect(photoedit.downloadFile.nth(0).visible)
.ok()
.click(photoedit.toggleExpandFile.nth(1))
.expect(photoedit.downloadFile.nth(1).visible)
.ok()
.expect(photoedit.deleteFile.visible)
.ok()
.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await photoviewer.openPhotoViewer("nth", 0);
await photoviewer.checkPhotoViewerActionAvailability("download", true);
await photoviewer.triggerPhotoViewerAction("close");
await menu.openPage("settings");
await t
.click(settings.uploadCheckbox)
.click(settings.downloadCheckbox)
.click(settings.editCheckbox)
.click(settings.shareCheckbox);
await t.eval(() => location.reload());
await t.navigateTo("/calendar");
await toolbar.checkToolbarActionAvailability("upload", false);
await album.checkHoverActionAvailability("nth", 2, "share", false);
await album.triggerHoverAction("nth", 0, "select");
await contextmenu.checkContextMenuActionAvailability("edit", true);
await contextmenu.checkContextMenuActionAvailability("download", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.clearSelection();
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", false);
await toolbar.checkToolbarActionAvailability("download", false);
await toolbar.checkToolbarActionAvailability("share", false);
await toolbar.checkToolbarActionAvailability("edit", true);
await t.navigateTo("/folders");
await toolbar.checkToolbarActionAvailability("upload", false);
await album.checkHoverActionAvailability("nth", 0, "share", false);
await album.triggerHoverAction("nth", 0, "select");
await contextmenu.checkContextMenuActionAvailability("edit", true);
await contextmenu.checkContextMenuActionAvailability("download", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.clearSelection();
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", false);
await toolbar.checkToolbarActionAvailability("download", false);
await toolbar.checkToolbarActionAvailability("share", false);
await toolbar.checkToolbarActionAvailability("edit", true);
await t.navigateTo("/albums");
await toolbar.checkToolbarActionAvailability("upload", false);
await album.checkHoverActionAvailability("nth", 0, "share", false);
await album.triggerHoverAction("nth", 0, "select");
await contextmenu.checkContextMenuActionAvailability("edit", true);
await contextmenu.checkContextMenuActionAvailability("delete", true);
await contextmenu.checkContextMenuActionAvailability("download", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.clearSelection();
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", false);
await toolbar.checkToolbarActionAvailability("download", false);
await toolbar.checkToolbarActionAvailability("share", false);
await toolbar.checkToolbarActionAvailability("edit", true);
await t.navigateTo("/browse");
await toolbar.checkToolbarActionAvailability("upload", false);
await toolbar.search("photo:true stack:true");
await photo.triggerHoverAction("nth", 0, "select");
await contextmenu.checkContextMenuActionAvailability("download", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("edit", false);
await contextmenu.clearSelection();
await t.click(page.cardTitle.nth(0));
await photoedit.checkAllDetailsFieldsDisabled(true);
await t.expect(photoedit.infoTab.visible).notOk();
await t.click(photoedit.filesTab);
await t
.expect(photoedit.downloadFile.nth(0).visible)
.notOk()
.click(photoedit.toggleExpandFile.nth(1))
.expect(photoedit.downloadFile.nth(1).visible)
.notOk()
.expect(photoedit.deleteFile.visible)
.ok();
await t.click(photoedit.dialogClose);
await toolbar.search("photo:true");
await photoviewer.openPhotoViewer("nth", 0);
await photoviewer.checkPhotoViewerActionAvailability("download", false);
await photoviewer.checkPhotoViewerActionAvailability("edit", true);
await photoviewer.triggerPhotoViewerAction("close");
await menu.openPage("settings");
await t
.click(settings.uploadCheckbox)
.click(settings.downloadCheckbox)
.click(settings.editCheckbox)
.click(settings.shareCheckbox);
}
);

View file

@ -0,0 +1,123 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import Menu from "../page-model/menu";
import Toolbar from "../page-model/toolbar";
import Photo from "../page-model/photo";
import PhotoViewer from "../page-model/photoviewer";
import Page from "../page-model/page";
import PhotoEdit from "../page-model/photo-edit";
import Library from "../page-model/library";
fixture`Test stacks`.page`${testcafeconfig.url}`;
const menu = new Menu();
const toolbar = new Toolbar();
const photo = new Photo();
const photoviewer = new PhotoViewer();
const page = new Page();
const photoedit = new PhotoEdit();
const library = new Library();
test.meta("testID", "stacks-001").meta({ type: "smoke" })(
"View all files of a stack",
async (t) => {
await toolbar.search("ski");
const SequentialPhotoUid = await photo.getNthPhotoUid("all", 0);
await photo.checkHoverActionAvailability("uid", SequentialPhotoUid, "open", true);
if (t.browser.platform === "desktop") {
console.log(t.browser.platform);
await photo.triggerHoverAction("nth", 0, "open");
await photoviewer.triggerPhotoViewerAction("next");
await photoviewer.triggerPhotoViewerAction("previous");
await photoviewer.triggerPhotoViewerAction("close");
}
await photo.checkHoverActionAvailability("uid", SequentialPhotoUid, "open", true);
}
);
test.meta("testID", "stacks-002").meta({ type: "smoke" })("Change primary file", async (t) => {
await toolbar.search("ski");
const SequentialPhotoUid = await photo.getNthPhotoUid("all", 0);
await toolbar.setFilter("view", "Cards");
await t
.click(page.cardTitle.withAttribute("data-uid", SequentialPhotoUid))
.click(photoedit.filesTab);
const FirstFileName = await Selector("div.caption").nth(0).innerText;
await t.expect(FirstFileName).contains("photos8_1_ski.jpg");
await t
.click(photoedit.toggleExpandFile.nth(1))
.click(photoedit.makeFilePrimary)
.click(photoedit.dialogClose)
.click(page.cardTitle.withAttribute("data-uid", SequentialPhotoUid));
const FirstFileNameAfterChange = await Selector("div.caption").nth(0).innerText;
await t
.expect(FirstFileNameAfterChange)
.notContains("photos8_1_ski.jpg")
.expect(FirstFileNameAfterChange)
.contains("photos8_2_ski.jpg");
});
test.meta("testID", "stacks-003").meta({ type: "smoke" })("Ungroup files", async (t) => {
await toolbar.search("group");
await toolbar.setFilter("view", "Cards");
const PhotoCount = await photo.getPhotoCount("all");
const SequentialPhotoUid = await photo.getNthPhotoUid("all", 0);
await t.expect(PhotoCount).eql(1);
await menu.openPage("stacks");
await photo.checkHoverActionAvailability("uid", SequentialPhotoUid, "open", true);
await t
.click(page.cardTitle.withAttribute("data-uid", SequentialPhotoUid))
.click(photoedit.filesTab)
.click(photoedit.toggleExpandFile.nth(0))
.click(photoedit.toggleExpandFile.nth(1))
.click(photoedit.unstackFile)
.wait(12000)
.click(photoedit.dialogClose);
await menu.openPage("browse");
await toolbar.search("group");
if (t.browser.platform === "mobile") {
await t.eval(() => location.reload());
} else {
await toolbar.triggerToolbarAction("reload");
}
const PhotoCountAfterUngroup = await photo.getPhotoCount("all");
await t.expect(PhotoCountAfterUngroup).eql(2);
await photo.checkHoverActionAvailability("uid", SequentialPhotoUid, "open", false);
});
test.meta("testID", "stacks-004")("Delete non primary file", async (t) => {
await menu.openPage("library");
await t
.click(library.importTab)
.click(library.openImportFolderSelect, { timeout: 5000 })
.click(page.selectOption.withText("/pizza"))
.click(library.import)
.wait(10000);
await menu.openPage("browse");
await toolbar.search("pizza");
await toolbar.setFilter("view", "Cards");
const PhotoCount = await photo.getPhotoCount("all");
const PhotoUid = await photo.getNthPhotoUid("all", 0);
await t.expect(PhotoCount).eql(1);
await t.click(page.cardTitle.withAttribute("data-uid", PhotoUid)).click(photoedit.filesTab);
const FileCount = await photoedit.getFileCount();
await t.expect(FileCount).eql(2);
await t
.click(photoedit.toggleExpandFile.nth(1))
.click(Selector(photoedit.deleteFile))
.click(Selector(".action-confirm"))
.wait(10000);
const FileCountAfterDeletion = await photoedit.getFileCount();
await t.expect(FileCountAfterDeletion).eql(1);
});

View file

@ -0,0 +1,148 @@
import { Selector } from "testcafe";
import testcafeconfig from "./testcafeconfig";
import Menu from "../page-model/menu";
import Album from "../page-model/album";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Photo from "../page-model/photo";
import Page from "../page-model/page";
import AlbumDialog from "../page-model/dialog-album";
fixture`Test states`.page`${testcafeconfig.url}`;
const menu = new Menu();
const album = new Album();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const photo = new Photo();
const page = new Page();
const albumdialog = new AlbumDialog();
test.meta("testID", "states-001")("Update state details", async (t) => {
await menu.openPage("states");
await toolbar.search("Canada");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await t.expect(page.cardTitle.nth(0).innerText).contains("British Columbia");
await t.click(page.cardTitle.nth(0));
await t
.expect(albumdialog.title.value)
.eql("British Columbia")
.expect(albumdialog.location.value)
.eql("Canada")
.expect(albumdialog.description.value)
.eql("")
.expect(albumdialog.category.value)
.eql("");
await t
.typeText(albumdialog.title, "Wonderland", { replace: true })
.typeText(albumdialog.location, "Earth", { replace: true })
.typeText(albumdialog.description, "We love earth")
.typeText(albumdialog.category, "Mountains")
.pressKey("enter")
.click(albumdialog.dialogSave);
await t
.expect(page.cardTitle.nth(0).innerText)
.contains("Wonderland")
.expect(page.cardDescription.nth(0).innerText)
.contains("We love earth")
.expect(Selector("div.caption").nth(1).innerText)
.contains("Mountains")
.expect(Selector("div.caption").nth(2).innerText)
.contains("Earth");
await album.openNthAlbum(0);
await t.expect(toolbar.toolbarTitle.innerText).contains("Wonderland");
await t.expect(toolbar.toolbarDescription.innerText).contains("We love earth");
await menu.openPage("states");
if (t.browser.platform === "mobile") {
await toolbar.search("category:Mountains");
} else {
await toolbar.setFilter("category", "Mountains");
}
await t.expect(page.cardTitle.nth(0).innerText).contains("Wonderland");
await album.openAlbumWithUid(AlbumUid);
await toolbar.triggerToolbarAction("edit");
await t
.expect(albumdialog.description.value)
.eql("We love earth")
.expect(albumdialog.category.value)
.eql("Mountains")
.expect(albumdialog.location.value)
.eql("Earth");
await t
.typeText(albumdialog.title, "British Columbia / Canada", { replace: true })
.click(albumdialog.category)
.pressKey("ctrl+a delete")
.pressKey("enter")
.click(albumdialog.description)
.pressKey("ctrl+a delete")
.pressKey("enter")
.typeText(albumdialog.location, "Canada", { replace: true })
.click(albumdialog.dialogSave);
await menu.openPage("states");
await toolbar.search("Canada");
await t
.expect(page.cardTitle.nth(0).innerText)
.contains("British Columbia / Canada")
.expect(page.cardDescription.innerText)
.notContains("We love earth")
.expect(Selector("div.caption").nth(0).innerText)
.notContains("Earth");
});
test.meta("testID", "states-002")("Create, Edit, delete sharing link for state", async (t) => {
await page.testCreateEditDeleteSharingLink("states");
});
test.meta("testID", "states-003")("Create/delete album-clone from state", async (t) => {
await menu.openPage("albums");
const AlbumCount = await album.getAlbumCount("all");
await menu.openPage("states");
await toolbar.search("Canada");
const FirstStateUid = await album.getNthAlbumUid("all", 0);
await album.openAlbumWithUid(FirstStateUid);
const PhotoCountInState = await photo.getPhotoCount("all");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
const SecondPhotoUid = await photo.getNthPhotoUid("image", 1);
await menu.openPage("states");
await album.selectAlbumFromUID(FirstStateUid);
await contextmenu.triggerContextMenuAction("clone", "NotYetExistingAlbumForState");
await menu.openPage("albums");
const AlbumCountAfterCreation = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterCreation).eql(AlbumCount + 1);
await toolbar.search("NotYetExistingAlbumForState");
const AlbumUid = await album.getNthAlbumUid("all", 0);
await album.openAlbumWithUid(AlbumUid);
const PhotoCountInAlbum = await photo.getPhotoCount("all");
await t.expect(PhotoCountInAlbum).eql(PhotoCountInState);
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
await menu.openPage("albums");
await album.selectAlbumFromUID(AlbumUid);
await contextmenu.triggerContextMenuAction("delete", "");
await menu.openPage("albums");
const AlbumCountAfterDelete = await album.getAlbumCount("all");
await t.expect(AlbumCountAfterDelete).eql(AlbumCount);
await menu.openPage("states");
await album.openAlbumWithUid(FirstStateUid);
await photo.checkPhotoVisibility(FirstPhotoUid, true);
await photo.checkPhotoVisibility(SecondPhotoUid, true);
});

View file

@ -0,0 +1,4 @@
{
"url": "localhost:2343/browse"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

View file

@ -0,0 +1,120 @@
[{
"SourceFile": "digikam.jpg",
"ExifToolVersion": 11.88,
"FileName": "digikam.jpg",
"Directory": ".",
"FileSize": "195 kB",
"FileModifyDate": "2020:10:19 10:29:43+02:00",
"FileAccessDate": "2020:10:19 10:29:43+02:00",
"FileInodeChangeDate": "2020:10:19 10:29:43+02:00",
"FilePermissions": "rw-rw-r--",
"FileType": "JPEG",
"FileTypeExtension": "jpg",
"MIMEType": "image/jpeg",
"JFIFVersion": 1.01,
"CurrentIPTCDigest": "92f4f2920e4cd46aa5c45f9d2ce7affa",
"CodedCharacterSet": "UTF8",
"Keywords": ["Berlin","Shop"],
"ExifByteOrder": "Big-endian (Motorola, MM)",
"Make": "HUAWEI",
"Model": "ELE-L29",
"Orientation": "Unknown (0)",
"XResolution": 72,
"YResolution": 72,
"ResolutionUnit": "inches",
"Software": "ELE-L29 10.1.0.150(C431E22R2P5)",
"ModifyDate": "2020:10:17 17:48:24",
"YCbCrPositioning": "Centered",
"DocumentName": "",
"ExposureTime": "1/50",
"FNumber": 1.8,
"ExposureProgram": "Program AE",
"ISO": 100,
"ExifVersion": "0210",
"DateTimeOriginal": "2020:10:17 17:48:24",
"CreateDate": "2020:10:17 17:48:24",
"ComponentsConfiguration": "Y, Cb, Cr, -",
"CompressedBitsPerPixel": 0.95,
"ShutterSpeedValue": "1/999963365",
"ApertureValue": 1.8,
"BrightnessValue": 0,
"ExposureCompensation": 0,
"MaxApertureValue": 1.8,
"MeteringMode": "Multi-segment",
"LightSource": "Daylight",
"Flash": "No Flash",
"FocalLength": "5.6 mm",
"MakerNoteUnknownText": "Auto",
"SubSecTime": 950488,
"SubSecTimeOriginal": 950488,
"SubSecTimeDigitized": 950488,
"FlashpixVersion": "0100",
"ColorSpace": "sRGB",
"ExifImageWidth": 3648,
"ExifImageHeight": 2736,
"InteropIndex": "R98 - DCF basic file (sRGB)",
"InteropVersion": "0100",
"SensingMethod": "One-chip color area",
"FileSource": "Digital Camera",
"SceneType": "Directly photographed",
"CustomRendered": "Custom",
"ExposureMode": "Auto",
"WhiteBalance": "Auto",
"DigitalZoomRatio": 1,
"FocalLengthIn35mmFormat": "27 mm",
"SceneCaptureType": "Standard",
"GainControl": "None",
"Contrast": "Normal",
"Saturation": "Normal",
"Sharpness": "Normal",
"SubjectDistanceRange": "Unknown",
"GPSVersionID": "2.2.0.0",
"GPSLatitudeRef": "North",
"GPSLongitudeRef": "East",
"GPSAltitudeRef": "Above Sea Level",
"GPSTimeStamp": "15:48:23",
"GPSProcessingMethod": "GPS",
"GPSDateStamp": "2020:10:17",
"DeviceSettingDescription": "(Binary data 4 bytes, use -b option to extract)",
"Compression": "JPEG (old-style)",
"ThumbnailOffset": 83310,
"ThumbnailLength": 27920,
"XMPToolkit": "XMP Core 4.4.0-Exiv2",
"Warning": "[minor] Fixed incorrect URI for xmlns:MicrosoftPhoto",
"Rating": 4,
"Categories": "<Categories><Category Assigned=\"1\">Berlin</Category><Category Assigned=\"1\">Shop</Category></Categories>",
"RatingPercent": 75,
"ColorLabel": 9,
"PickLabel": 3,
"Urgency": "9 (user-defined priority)",
"LastKeywordXMP": ["Berlin","Shop"],
"TagsList": ["Berlin","Shop"],
"HierarchicalSubject": ["Berlin","Shop"],
"CatalogSets": ["Berlin","Shop"],
"Subject": ["Berlin","Shop"],
"ImageWidth": 500,
"ImageHeight": 375,
"EncodingProcess": "Baseline DCT, Huffman coding",
"BitsPerSample": 8,
"ColorComponents": 3,
"YCbCrSubSampling": "YCbCr4:2:0 (2 2)",
"Aperture": 1.8,
"ImageSize": "500x375",
"Megapixels": 0.188,
"ScaleFactor35efl": 4.8,
"ShutterSpeed": "1/50",
"SubSecCreateDate": "2020:10:17 17:48:24.950488",
"SubSecDateTimeOriginal": "2020:10:17 17:48:24.950488",
"SubSecModifyDate": "2020:10:17 17:48:24.950488",
"ThumbnailImage": "(Binary data 27920 bytes, use -b option to extract)",
"GPSAltitude": "84.4 m Above Sea Level",
"GPSDateTime": "2020:10:17 15:48:23Z",
"GPSLatitude": "52 deg 27' 37.88\" N",
"GPSLongitude": "13 deg 19' 53.05\" E",
"CircleOfConfusion": "0.006 mm",
"FOV": "67.4 deg",
"FocalLength35efl": "5.6 mm (35 mm equivalent: 27.0 mm)",
"GPSPosition": "52 deg 27' 37.88\" N, 13 deg 19' 53.05\" E",
"HyperfocalDistance": "2.79 m",
"LightValue": 7.3
}]

View file

@ -0,0 +1 @@
example text file, you can ignore this

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View file

@ -0,0 +1,123 @@
import { Selector } from "testcafe";
import testcafeconfig from "../acceptance/testcafeconfig";
import Page from "../acceptance/page-model";
fixture`Test authentication with openid`.page`https://photoprism.traefik.net`;
const page = new Page();
test.meta("testID", "authentication-000")(
"Time to start instance (will be marked as unstable)",
async (t) => {
await t.wait(5000);
}
);
test.meta("testID", "authentication-001")("Login and Logout as admin", async (t) => {
await t
.expect(Selector("#username").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk()
.typeText(Selector("#username"), "admin", { replace: true })
.typeText(Selector("#password"), "photoprism", { replace: true })
.click(Selector("#kc-login"))
.expect(Selector(".input-search input", { timeout: 7000 }).visible)
.ok();
await page.openNav();
await t
.click(Selector('div[title="Logout"]'))
.expect(Selector(".input-name input").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
await t.navigateTo("/settings");
await t
.expect(Selector("#username").visible)
.notOk()
.expect(Selector(".input-search input").visible)
.ok();
await t
/* TODO
await t.navigateTo("/library");
.expect(Selector(".input-index-folder input").visible)
.ok()
.expect(Selector("div.p-page-photos").visible)
.notOk()*/
.click(Selector('div[title="Logout"]'))
.expect(Selector(".input-name input").visible)
.ok()
.navigateTo("https://keycloak.traefik.net")
.click(Selector('a[href="https://keycloak.traefik.net/auth/admin/"]'))
.click(Selector("a.dropdown-toggle"))
.click(Selector("a").withText("Sign Out"));
await t.navigateTo("https://photoprism.traefik.net/settings");
await t
.expect(Selector("#username").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
});
test.meta("testID", "authentication-001")("Login and Logout as user", async (t) => {
await t
.expect(Selector("#username").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk()
.typeText(Selector("#username"), "user", { replace: true })
.typeText(Selector("#password"), "photoprism", { replace: true })
.click(Selector("#kc-login"))
.expect(Selector(".input-search input", { timeout: 7000 }).visible)
.ok();
await page.openNav();
await t
.click(Selector('div[title="Logout"]'))
.expect(Selector(".input-name input").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
await t.navigateTo("/settings");
await t
.expect(Selector("#username").visible)
.notOk()
.expect(Selector(".input-search input").visible)
.ok();
await t.navigateTo("/library");
await t
.expect(Selector(".input-index-folder input").visible)
.notOk()
.expect(Selector("div.p-page-photos").visible)
.ok()
.click(Selector('div[title="Logout"]'))
.expect(Selector(".input-name input").visible)
.ok()
.navigateTo("https://keycloak.traefik.net")
.click(Selector('a[href="https://keycloak.traefik.net/auth/admin/"]'))
.click(Selector("a.dropdown-toggle"))
.click(Selector("a").withText("Sign Out"));
await t.navigateTo("https://photoprism.traefik.net/settings");
await t
.expect(Selector("#username").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
});
test.meta("testID", "authentication-002")("Login with wrong credentials", async (t) => {
await t
.expect(Selector("#username").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk()
.typeText(Selector("#username"), "admin", { replace: true })
.typeText(Selector("#password"), "wrong", { replace: true })
.click(Selector("#kc-login"))
.expect(Selector(".input-search input", { timeout: 7000 }).visible)
.notOk();
await t
.navigateTo("https://photoprism.traefik.net/favorites")
.expect(Selector(".input-name input").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
});

View file

@ -0,0 +1,691 @@
import { Selector } from "testcafe";
import { ClientFunction } from "testcafe";
import testcafeconfig from "../acceptance/testcafeconfig";
import Menu from "../page-model/menu";
import Photo from "../page-model/photo";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Label from "../page-model/label";
import Album from "../page-model/album";
import Subject from "../page-model/subject";
import Page from "../page-model/page";
import Settings from "../page-model/settings";
import Library from "../page-model/library";
import PhotoEdit from "../page-model/photo-edit";
import AlbumDialog from "../page-model/dialog-album";
fixture`Test admin role`.page`${testcafeconfig.url}`;
const menu = new Menu();
const photo = new Photo();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const label = new Label();
const album = new Album();
const subject = new Subject();
const page = new Page();
const settings = new Settings();
const library = new Library();
const photoedit = new PhotoEdit();
const albumdialog = new AlbumDialog();
const getLocation = ClientFunction(() => document.location.href);
test.meta("testID", "authentication-000")(
"Time to start instance (will be marked as unstable)",
async (t) => {
await t.wait(5000);
}
);
test.meta("testID", "admin-role-001").meta({ type: "smoke" })("Access to settings", async (t) => {
await page.login("admin", "photoprism");
await menu.checkMenuItemAvailability("settings", true);
await t.navigateTo("/settings");
await t
.expect(settings.languageInput.visible)
.ok()
.expect(Selector("div.p-page-photos").visible)
.notOk();
await t.navigateTo("/settings/library");
await t
.expect(Selector("form.p-form-settings").visible)
.ok()
.expect(Selector("div.p-page-photos").visible)
.notOk();
await t.navigateTo("/settings/advanced");
await t
.expect(Selector("label").withText("Read-Only Mode").visible)
.ok()
.expect(Selector("div.p-page-photos").visible)
.notOk();
await t.navigateTo("/settings/sync");
await t
.expect(Selector("div.p-accounts-list").visible)
.ok()
.expect(Selector("div.p-page-photos").visible)
.notOk();
});
test.meta("testID", "admin-role-002").meta({ type: "smoke" })("Access to archive", async (t) => {
await page.login("admin", "photoprism");
const PhotoCountBrowse = await photo.getPhotoCount("all");
await menu.checkMenuItemAvailability("archive", true);
await t.navigateTo("/archive");
await photo.checkPhotoVisibility("pqnahct2mvee8sr4", true);
const PhotoCountArchive = await photo.getPhotoCount("all");
await t.expect(PhotoCountBrowse).gte(PhotoCountArchive);
});
test.meta("testID", "admin-role-003").meta({ type: "smoke" })("Access to review", async (t) => {
await page.login("admin", "photoprism");
const PhotoCountBrowse = await photo.getPhotoCount("all");
await menu.checkMenuItemAvailability("review", true);
await t.navigateTo("/review");
await photo.checkPhotoVisibility("pqzuein2pdcg1kc7", true);
const PhotoCountReview = await photo.getPhotoCount("all");
await t.expect(PhotoCountBrowse).gte(PhotoCountReview);
});
test.meta("testID", "admin-role-004").meta({ type: "smoke" })("Access to private", async (t) => {
await page.login("admin", "photoprism");
const PhotoCountBrowse = await photo.getPhotoCount("all");
await menu.checkMenuItemAvailability("private", true);
await t.navigateTo("/private");
await photo.checkPhotoVisibility("pqmxlquf9tbc8mk2", true);
const PhotoCountPrivate = await photo.getPhotoCount("all");
await t.expect(PhotoCountBrowse).gte(PhotoCountPrivate);
});
test.meta("testID", "admin-role-005").meta({ type: "smoke" })("Access to library", async (t) => {
await page.login("admin", "photoprism");
const PhotoCountBrowse = await photo.getPhotoCount("all");
await menu.checkMenuItemAvailability("library", true);
await t.navigateTo("/library");
await t
.expect(library.indexFolderSelect.visible)
.ok()
.expect(Selector("div.p-page-photos").visible)
.notOk();
await t.navigateTo("/library/import");
await t
.expect(library.openImportFolderSelect.visible)
.ok()
.expect(Selector("div.p-page-photos").visible)
.notOk();
await t.navigateTo("/library/logs");
await t
.expect(Selector("div.terminal").visible)
.ok()
.expect(Selector("div.p-page-photos").visible)
.notOk();
await menu.checkMenuItemAvailability("originals", true);
await t.navigateTo("/library/files");
await t
.expect(Selector("div.p-page-files").visible)
.ok()
.expect(Selector("div.p-page-photos").visible)
.notOk();
await menu.checkMenuItemAvailability("hidden", true);
await t.navigateTo("/library/hidden");
const PhotoCountHidden = await photo.getPhotoCount("all");
await t.expect(PhotoCountBrowse).gte(PhotoCountHidden);
await menu.checkMenuItemAvailability("errors", true);
await t.navigateTo("/library/errors");
await t
.expect(Selector("div.p-page-errors").visible)
.ok()
.expect(Selector("div.p-page-photos").visible)
.notOk();
});
test.meta("testID", "admin-role-006").meta({ type: "smoke" })(
"private/archived photos in search results",
async (t) => {
await page.login("admin", "photoprism");
const PhotoCountBrowse = await photo.getPhotoCount("all");
await toolbar.search("private:true");
const PhotoCountPrivate = await photo.getPhotoCount("all");
await t.expect(PhotoCountPrivate).eql(2);
await photo.checkPhotoVisibility("pqmxlquf9tbc8mk2", true);
await toolbar.search("archived:true");
const PhotoCountArchive = await photo.getPhotoCount("all");
await t.expect(PhotoCountArchive).eql(3);
await photo.checkPhotoVisibility("pqnahct2mvee8sr4", true);
await toolbar.search("quality:0");
const PhotoCountReview = await photo.getPhotoCount("all");
await t.expect(PhotoCountReview).gte(PhotoCountBrowse);
await photo.checkPhotoVisibility("pqzuein2pdcg1kc7", true);
await menu.openPage("places");
await t
.expect(Selector("#map").exists, { timeout: 15000 })
.ok()
.expect(Selector("div.p-map-control").visible)
.ok()
.wait(5000);
await t
.typeText(Selector('input[aria-label="Search"]'), "oaxaca", { replace: true })
.pressKey("enter");
await t
.expect(Selector("div.p-map-control").visible)
.ok()
.expect(getLocation())
.contains("oaxaca")
.wait(5000)
.expect(Selector('div[title="Viewpoint / Mexico / 2017"]').visible)
.notOk()
.expect(Selector('div[title="Viewpoint / Mexico / 2018"]').visible)
.notOk();
await t
.typeText(Selector('input[aria-label="Search"]'), "canada", { replace: true })
.pressKey("enter");
await t
.expect(Selector("div.p-map-control").visible)
.ok()
.expect(getLocation())
.contains("canada")
.wait(8000)
.expect(Selector('div[title="Cape / Bowen Island / 2019"]').visible)
.ok()
.expect(Selector('div[title="Truck / Vancouver / 2019"]').visible)
.notOk();
}
);
test.meta("testID", "admin-role-007").meta({ type: "smoke" })("Upload functionality", async (t) => {
await page.login("admin", "photoprism");
await toolbar.checkToolbarActionAvailability("upload", true);
await menu.openPage("albums");
await toolbar.checkToolbarActionAvailability("upload", true);
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", true);
await menu.openPage("video");
await toolbar.checkToolbarActionAvailability("upload", true);
await menu.openPage("favorites");
await toolbar.checkToolbarActionAvailability("upload", true);
await menu.openPage("moments");
await toolbar.checkToolbarActionAvailability("upload", true);
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", true);
await menu.openPage("calendar");
await toolbar.checkToolbarActionAvailability("upload", true);
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", true);
await menu.openPage("states");
await toolbar.checkToolbarActionAvailability("upload", true);
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", true);
await menu.openPage("folders");
await toolbar.checkToolbarActionAvailability("upload", true);
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", true);
});
test.meta("testID", "admin-role-008").meta({ type: "smoke" })(
"Admin can private, archive, share, add/remove to album",
async (t) => {
await page.login("admin", "photoprism");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
await photo.selectPhotoFromUID(FirstPhotoUid);
await contextmenu.checkContextMenuActionAvailability("private", true);
await contextmenu.checkContextMenuActionAvailability("archive", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("album", true);
await contextmenu.clearSelection();
await toolbar.setFilter("view", "List");
await photo.checkListViewActionAvailability("private", false);
await menu.openPage("albums");
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", true);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("private", true);
await contextmenu.checkContextMenuActionAvailability("remove", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("album", true);
await contextmenu.clearSelection();
await toolbar.triggerToolbarAction("list-view");
await photo.checkListViewActionAvailability("private", false);
}
);
test.meta("testID", "admin-role-009").meta({ type: "smoke" })(
"Admin can approve low quality photos",
async (t) => {
await page.login("admin", "photoprism");
await toolbar.search('quality:0 name:"photos-013_1"');
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.triggerContextMenuAction("edit", "");
await t.expect(photoedit.detailsApprove.visible).ok();
}
);
test.meta("testID", "admin-role-010").meta({ type: "smoke" })(
"Edit dialog is not read only for admin",
async (t) => {
await page.login("admin", "photoprism");
await toolbar.search("faces:new");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
await photo.selectPhotoFromUID(FirstPhotoUid);
await contextmenu.triggerContextMenuAction("edit", "");
await photoedit.checkAllDetailsFieldsDisabled(false);
await t.expect(photoedit.detailsApply.visible).ok();
if (t.browser.platform !== "mobile") {
await t.expect(photoedit.detailsDone.visible).ok();
}
await t.click(photoedit.labelsTab);
await photoedit.checkFieldDisabled(photoedit.removeLabel, false);
await t.expect(photoedit.inputLabelName.exists).ok().expect(photoedit.addLabel.exists).ok();
await t.click(photoedit.openInlineEdit);
await t.expect(photoedit.inputLabelRename.exists).ok();
await t.click(photoedit.peopleTab);
await photoedit.checkFieldDisabled(photoedit.inputName, false);
await t.expect(photoedit.removeMarker.exists).ok();
await t.click(photoedit.infoTab);
await photoedit.checkAllInfoFieldsDisabled(false);
}
);
test.meta("testID", "admin-role-011").meta({ type: "smoke" })(
"Edit labels functionality",
async (t) => {
await page.login("admin", "photoprism");
await menu.openPage("labels");
const FirstLabelUid = await label.getNthLabeltUid(0);
await label.checkHoverActionState("uid", FirstLabelUid, "favorite", false);
await label.triggerHoverAction("uid", FirstLabelUid, "favorite");
await label.checkHoverActionState("uid", FirstLabelUid, "favorite", true);
await label.triggerHoverAction("uid", FirstLabelUid, "favorite");
await label.checkHoverActionState("uid", FirstLabelUid, "favorite", false);
await t.click(Selector(`a.uid-${FirstLabelUid} div.inline-edit`));
await t.expect(photoedit.inputLabelRename.visible).ok();
await label.selectLabelFromUID(FirstLabelUid);
await contextmenu.checkContextMenuActionAvailability("delete", true);
await contextmenu.checkContextMenuActionAvailability("album", true);
}
);
test.meta("testID", "admin-role-012").meta({ type: "smoke" })(
"Edit album functionality",
async (t) => {
await page.login("admin", "photoprism");
await menu.openPage("albums");
await toolbar.checkToolbarActionAvailability("add", true);
await album.checkHoverActionAvailability("nth", 1, "share", true);
const FirstAlbumUid = await album.getNthAlbumUid("all", 0);
await album.selectAlbumFromUID(FirstAlbumUid);
await contextmenu.checkContextMenuActionAvailability("edit", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("clone", true);
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("delete", true);
await contextmenu.clearSelection();
await t.click(page.cardTitle);
await t.expect(albumdialog.description.visible).ok();
await t.click(albumdialog.dialogCancel);
if (await Selector(`a.uid-${FirstAlbumUid}`).hasClass("is-favorite")) {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
} else {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
}
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", true);
await toolbar.checkToolbarActionAvailability("edit", true);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("album", true);
await contextmenu.checkContextMenuActionAvailability("private", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("remove", true);
}
);
test.meta("testID", "admin-role-013")("Edit moment functionality", async (t) => {
await page.login("admin", "photoprism");
await menu.openPage("moments");
await album.checkHoverActionAvailability("nth", 0, "share", true);
const FirstAlbumUid = await album.getNthAlbumUid("moment", 0);
await album.selectAlbumFromUID(FirstAlbumUid);
await contextmenu.checkContextMenuActionAvailability("edit", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("clone", true);
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("delete", true);
await contextmenu.clearSelection();
await t.click(page.cardTitle);
await t.expect(albumdialog.description.visible).ok();
await t.click(albumdialog.dialogCancel);
if (await Selector(`a.uid-${FirstAlbumUid}`).hasClass("is-favorite")) {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
} else {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
}
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", true);
await toolbar.checkToolbarActionAvailability("edit", true);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("album", true);
await contextmenu.checkContextMenuActionAvailability("private", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("archive", true);
await contextmenu.checkContextMenuActionAvailability("remove", false);
});
test.meta("testID", "admin-role-014").meta({ type: "smoke" })(
"Edit state functionality",
async (t) => {
await page.login("admin", "photoprism");
await menu.openPage("states");
await album.checkHoverActionAvailability("nth", 0, "share", true);
const FirstAlbumUid = await album.getNthAlbumUid("state", 0);
await album.selectAlbumFromUID(FirstAlbumUid);
await contextmenu.checkContextMenuActionAvailability("edit", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("clone", true);
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("delete", true);
await contextmenu.clearSelection();
await t.click(page.cardTitle);
await t.expect(albumdialog.description.visible).ok();
await t.click(albumdialog.dialogCancel);
if (await Selector(`a.uid-${FirstAlbumUid}`).hasClass("is-favorite")) {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
} else {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
}
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", true);
await toolbar.checkToolbarActionAvailability("edit", true);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("album", true);
await contextmenu.checkContextMenuActionAvailability("private", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("archive", true);
await contextmenu.checkContextMenuActionAvailability("remove", false);
}
);
test.meta("testID", "admin-role-015")("Edit calendar functionality", async (t) => {
await page.login("admin", "photoprism");
await menu.openPage("calendar");
await album.checkHoverActionAvailability("nth", 0, "share", true);
const FirstAlbumUid = await album.getNthAlbumUid("month", 0);
await album.selectAlbumFromUID(FirstAlbumUid);
await contextmenu.checkContextMenuActionAvailability("edit", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("clone", true);
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("delete", false);
await contextmenu.clearSelection();
await t.click(page.cardTitle);
await t.expect(albumdialog.description.visible).ok();
await t.click(albumdialog.dialogCancel);
if (await Selector(`a.uid-${FirstAlbumUid}`).hasClass("is-favorite")) {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
} else {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
}
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", true);
await toolbar.checkToolbarActionAvailability("edit", true);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("album", true);
await contextmenu.checkContextMenuActionAvailability("private", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("archive", true);
});
test.meta("testID", "admin-role-016")("Edit folder functionality", async (t) => {
await page.login("admin", "photoprism");
await menu.openPage("folders");
await album.checkHoverActionAvailability("nth", 0, "share", true);
const FirstAlbumUid = await album.getNthAlbumUid("folder", 0);
await album.selectAlbumFromUID(FirstAlbumUid);
await contextmenu.checkContextMenuActionAvailability("edit", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("clone", true);
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("delete", false);
await contextmenu.clearSelection();
await t.click(page.cardTitle);
await t.expect(albumdialog.description.visible).ok();
await t.click(albumdialog.dialogCancel);
if (await Selector(`a.uid-${FirstAlbumUid}`).hasClass("is-favorite")) {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
} else {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
}
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", true);
await toolbar.checkToolbarActionAvailability("edit", true);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("album", true);
await contextmenu.checkContextMenuActionAvailability("private", true);
await contextmenu.checkContextMenuActionAvailability("share", true);
await contextmenu.checkContextMenuActionAvailability("archive", true);
});
test.meta("testID", "admin-role-017").meta({ type: "smoke" })(
"Edit people functionality",
async (t) => {
await page.login("admin", "photoprism");
await menu.openPage("people");
await toolbar.checkToolbarActionAvailability("show-hidden", true);
await t.expect(subject.newTab.exists).ok();
await subject.checkSubjectVisibility("name", "Otto Visible", true);
await subject.checkSubjectVisibility("name", "Monika Hide", false);
await toolbar.triggerToolbarAction("show-hidden");
await subject.checkSubjectVisibility("name", "Otto Visible", true);
await subject.checkSubjectVisibility("name", "Monika Hide", true);
await t.click(Selector("a div.v-card__title").nth(0));
await t.expect(Selector("div.input-rename input").visible).ok();
await subject.checkHoverActionAvailability("nth", 0, "hidden", true);
await subject.toggleSelectNthSubject(0);
await contextmenu.checkContextMenuActionAvailability("album", "true");
await contextmenu.clearSelection();
const FirstSubjectUid = await subject.getNthSubjectUid(0);
if (await Selector(`a.uid-${FirstSubjectUid}`).hasClass("is-favorite")) {
await subject.checkHoverActionState("uid", FirstSubjectUid, "favorite", true);
await subject.triggerHoverAction("uid", FirstSubjectUid, "favorite");
await subject.checkHoverActionState("uid", FirstSubjectUid, "favorite", false);
await subject.triggerHoverAction("uid", FirstSubjectUid, "favorite");
await subject.checkHoverActionState("uid", FirstSubjectUid, "favorite", true);
} else {
await subject.checkHoverActionState("uid", FirstSubjectUid, "favorite", false);
await subject.triggerHoverAction("uid", FirstSubjectUid, "favorite");
await subject.checkHoverActionState("uid", FirstSubjectUid, "favorite", true);
await subject.triggerHoverAction("uid", FirstSubjectUid, "favorite");
await subject.checkHoverActionState("uid", FirstSubjectUid, "favorite", false);
}
await subject.openNthSubject(0);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.peopleTab);
await photoedit.checkFieldDisabled(photoedit.inputName, false);
await t.expect(photoedit.rejectName.hasClass("v-icon--disabled")).notOk();
await t.navigateTo("/people/new");
await t.expect(Selector("div.is-face").visible).ok();
await t.navigateTo("/people?hidden=yes&order=relevance");
await t.expect(Selector("a.is-subject").visible).ok();
}
);

View file

@ -1,120 +1,135 @@
import { Selector } from "testcafe";
import testcafeconfig from "../acceptance/testcafeconfig";
import Page from "../acceptance/page-model";
import Page from "../page-model/page";
import Account from "../page-model/account";
import Menu from "../page-model/menu";
fixture`Test authentication`.page`${testcafeconfig.url}`;
const page = new Page();
test.meta("testID", "authentication-000")(
"Time to start instance (will be marked as unstable)",
const account = new Account();
const menu = new Menu();
test.meta("testID", "authentication-001").meta({ type: "smoke" })("Login and Logout", async (t) => {
await t.navigateTo("/browse");
await t
.expect(page.nameInput.visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
await t.typeText(page.nameInput, "admin", { replace: true });
await t.expect(page.loginAction.hasAttribute("disabled", "disabled")).ok();
await t.typeText(page.passwordInput, "photoprism", { replace: true });
await t.expect(page.passwordInput.hasAttribute("type", "password")).ok();
await t.click(page.togglePasswordMode);
await t.expect(page.passwordInput.hasAttribute("type", "text")).ok();
await t.click(page.togglePasswordMode);
await t.expect(page.passwordInput.hasAttribute("type", "password")).ok();
await t.click(page.loginAction);
await t.expect(Selector(".input-search input", { timeout: 7000 }).visible).ok();
await page.logout();
await t
.expect(page.nameInput.visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
await t.navigateTo("/settings");
await t
.expect(page.nameInput.visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
});
test.meta("testID", "authentication-002").meta({ type: "smoke" })(
"Login with wrong credentials",
async (t) => {
await t.wait(5000);
await page.login("wrong", "photoprism");
await t.navigateTo("/favorites");
await t
.expect(page.nameInput.visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
await page.login("admin", "abcdefg");
await t.navigateTo("/archive");
await t
.expect(page.nameInput.visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
}
);
test.meta("testID", "authentication-001")("Login and Logout", async (t) => {
//await t.wait(800000);
test.meta("testID", "authentication-003").meta({ type: "smoke" })("Change password", async (t) => {
await t.navigateTo("/browse");
await t
.expect(Selector(".input-name input").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk()
.typeText(Selector(".input-name input"), "admin", { replace: true })
.expect(Selector(".action-confirm").hasAttribute("disabled", "disabled"))
.ok()
.typeText(Selector(".input-password input"), "photoprism", { replace: true })
.expect(Selector(".input-password input").hasAttribute("type", "password"))
.ok()
.click(Selector(".v-input__icon--append"))
.expect(Selector(".input-password input").hasAttribute("type", "text"))
.ok()
.click(Selector(".v-input__icon--append"))
.expect(Selector(".input-password input").hasAttribute("type", "password"))
.ok()
.click(Selector(".action-confirm"))
.expect(Selector(".input-search input", { timeout: 7000 }).visible)
.ok();
await page.openNav();
await t
.click(Selector('div[title="Logout"]'))
.expect(Selector(".input-name input").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
await t.navigateTo("/settings");
await t
.expect(Selector(".input-name input").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
});
//TODO test all pages not accessible while logged out
test.meta("testID", "authentication-002")("Login with wrong credentials", async (t) => {
await page.login("wrong", "photoprism");
await t
.navigateTo("/favorites")
.expect(Selector(".input-name input").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
await page.login("admin", "abcdefg");
await t
.navigateTo("/archive")
.expect(Selector(".input-name input").visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
});
test.meta("testID", "authentication-003")("Change password", async (t) => {
await page.login("admin", "photoprism");
await t.expect(Selector(".input-search input").visible).ok();
await page.openNav();
await t.expect(Selector(".input-search input", { timeout: 15000 }).visible).ok();
await menu.openPage("profile");
await t
.click(Selector(".nav-settings"))
.click(Selector("#tab-settings-account"))
.typeText(Selector(".input-current-password input"), "wrong", { replace: true })
.typeText(Selector(".input-new-password input"), "photoprism", { replace: true })
.expect(Selector(".action-confirm").hasAttribute("disabled", "disabled"))
.ok()
.typeText(Selector(".input-retype-password input"), "photoprism", { replace: true })
.expect(Selector(".action-confirm").hasAttribute("disabled", "disabled"))
.notOk()
.click(".action-confirm")
.typeText(Selector(".input-current-password input"), "photoprism", { replace: true })
.typeText(Selector(".input-new-password input"), "photoprism123", { replace: true })
.expect(Selector(".action-confirm").hasAttribute("disabled", "disabled"))
.ok()
.typeText(Selector(".input-retype-password input"), "photoprism123", { replace: true })
.expect(Selector(".action-confirm").hasAttribute("disabled", "disabled"))
.notOk()
.click(".action-confirm");
await page.openNav();
await t.click(Selector('div[title="Logout"]'));
.typeText(account.currentPassword, "wrong", { replace: true })
.typeText(account.newPassword, "photoprism", { replace: true });
await t.expect(account.confirm.hasAttribute("disabled", "disabled")).ok();
await t.typeText(account.retypePassword, "photoprism", { replace: true });
await t.expect(account.confirm.hasAttribute("disabled", "disabled")).notOk();
await t
.click(account.confirm)
.typeText(account.currentPassword, "photoprism", { replace: true })
.typeText(account.newPassword, "photoprism123", { replace: true });
await t.expect(account.confirm.hasAttribute("disabled", "disabled")).ok();
await t.typeText(account.retypePassword, "photoprism123", { replace: true });
await t.expect(account.confirm.hasAttribute("disabled", "disabled")).notOk();
await t.click(account.confirm);
await page.logout();
if (t.browser.platform === "mobile") {
await t.wait(7000);
}
await page.login("admin", "photoprism");
await t.navigateTo("/archive");
await t
.navigateTo("/archive")
.expect(Selector(".input-name input").visible)
.expect(page.nameInput.visible)
.ok()
.expect(Selector(".input-search input").visible)
.notOk();
await page.login("admin", "photoprism123");
await t.expect(Selector(".input-search input").visible).ok();
await page.openNav();
await menu.openPage("profile");
await t
.click(Selector(".nav-settings", { timeout: 7000 }))
.click(Selector("#tab-settings-account"))
.typeText(Selector(".input-current-password input"), "photoprism123", { replace: true })
.typeText(Selector(".input-new-password input"), "photoprism", { replace: true })
.typeText(Selector(".input-retype-password input"), "photoprism", { replace: true })
.click(".action-confirm");
await page.openNav();
await t.click(Selector('div[title="Logout"]'));
.typeText(account.currentPassword, "photoprism123", { replace: true })
.typeText(account.newPassword, "photoprism", { replace: true })
.typeText(account.retypePassword, "photoprism", { replace: true })
.click(account.confirm);
await page.logout();
await page.login("admin", "photoprism");
await t.expect(Selector(".input-search input").visible).ok();
await page.openNav();
await t.click(Selector('div[title="Logout"]'));
await page.logout();
});

View file

@ -0,0 +1,803 @@
import { Selector } from "testcafe";
import testcafeconfig from "../acceptance/testcafeconfig";
import { ClientFunction } from "testcafe";
import Menu from "../page-model/menu";
import Photo from "../page-model/photo";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Label from "../page-model/label";
import Album from "../page-model/album";
import Subject from "../page-model/subject";
import PhotoViewer from "../page-model/photoviewer";
import PhotoEdit from "../page-model/photo-edit";
import Page from "../page-model/page";
import Settings from "../page-model/settings";
import Library from "../page-model/library";
import AlbumDialog from "../page-model/dialog-album";
fixture`Test member role`.page`${testcafeconfig.url}`;
const menu = new Menu();
const photo = new Photo();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const label = new Label();
const album = new Album();
const subject = new Subject();
const photoviewer = new PhotoViewer();
const photoedit = new PhotoEdit();
const page = new Page();
const settings = new Settings();
const library = new Library();
const albumdialog = new AlbumDialog();
const getLocation = ClientFunction(() => document.location.href);
test.meta("testID", "member-role-001").meta({ type: "smoke" })(
"No access to settings",
async (t) => {
await page.login("member", "passwdmember");
await menu.checkMenuItemAvailability("settings", false);
await t.navigateTo("/settings");
await t
.expect(settings.languageInput.visible)
.notOk()
.expect(Selector("div.p-page-photos").visible)
.ok();
await t.navigateTo("/settings/library");
await t
.expect(Selector("form.p-form-settings").visible)
.notOk()
.expect(Selector("div.p-page-photos").visible)
.ok();
await t.navigateTo("/settings/advanced");
await t
.expect(Selector("label").withText("Read-Only Mode").visible)
.notOk()
.expect(Selector("div.p-page-photos").visible)
.ok();
await t.navigateTo("/settings/sync");
await t
.expect(Selector("div.p-accounts-list").visible)
.notOk()
.expect(Selector("div.p-page-photos").visible)
.ok();
}
);
test.meta("testID", "member-role-002").meta({ type: "smoke" })(
"No access to archive",
async (t) => {
await page.login("member", "passwdmember");
if (t.browser.platform === "mobile") {
await t.wait(5000);
}
const PhotoCountBrowse = await photo.getPhotoCount("all");
await menu.checkMenuItemAvailability("archive", false);
await t.navigateTo("/archive");
await photo.checkPhotoVisibility("pqnahct2mvee8sr4", false);
if (t.browser.platform === "mobile") {
await t.wait(5000);
}
const PhotoCountArchive = await photo.getPhotoCount("all");
await t.expect(PhotoCountBrowse).eql(PhotoCountArchive);
}
);
test.meta("testID", "member-role-003").meta({ type: "smoke" })("No access to review", async (t) => {
await page.login("member", "passwdmember");
if (t.browser.platform === "mobile") {
await t.wait(5000);
}
const PhotoCountBrowse = await photo.getPhotoCount("all");
await menu.checkMenuItemAvailability("review", false);
await t.navigateTo("/review");
await photo.checkPhotoVisibility("pqzuein2pdcg1kc7", false);
if (t.browser.platform === "mobile") {
await t.wait(5000);
}
const PhotoCountReview = await photo.getPhotoCount("all");
await t.expect(PhotoCountBrowse).eql(PhotoCountReview);
});
test.meta("testID", "member-role-004").meta({ type: "smoke" })(
"No access to private",
async (t) => {
await page.login("member", "passwdmember");
if (t.browser.platform === "mobile") {
await t.wait(5000);
}
const PhotoCountBrowse = await photo.getPhotoCount("all");
await menu.checkMenuItemAvailability("private", false);
await t.navigateTo("/private");
await photo.checkPhotoVisibility("pqmxlquf9tbc8mk2", false);
if (t.browser.platform === "mobile") {
await t.wait(5000);
}
const PhotoCountPrivate = await photo.getPhotoCount("all");
await t.expect(PhotoCountBrowse).eql(PhotoCountPrivate);
}
);
test.meta("testID", "member-role-005").meta({ type: "smoke" })(
"No access to library",
async (t) => {
await page.login("member", "passwdmember");
if (t.browser.platform === "mobile") {
await t.wait(5000);
}
const PhotoCountBrowse = await photo.getPhotoCount("all");
await menu.checkMenuItemAvailability("library", false);
await t.navigateTo("/library");
await t
.expect(library.indexFolderSelect.visible)
.notOk()
.expect(Selector("div.p-page-photos").visible)
.ok();
await t.navigateTo("/library/import");
await t
.expect(library.openImportFolderSelect.visible)
.notOk()
.expect(Selector("div.p-page-photos").visible)
.ok();
await t.navigateTo("/library/logs");
await t
.expect(Selector("p.p-log-message").visible)
.notOk()
.expect(Selector("div.p-page-photos").visible)
.ok();
await menu.checkMenuItemAvailability("originals", false);
await t.navigateTo("/library/files");
await t
.expect(Selector("div.p-page-files").visible)
.notOk()
.expect(Selector("div.p-page-photos").visible)
.ok();
await menu.checkMenuItemAvailability("hidden", false);
await t.navigateTo("/library/hidden");
if (t.browser.platform === "mobile") {
await t.wait(5000);
}
const PhotoCountHidden = await photo.getPhotoCount("all");
await t.expect(PhotoCountBrowse).eql(PhotoCountHidden);
await menu.checkMenuItemAvailability("errors", false);
await t.navigateTo("/library/errors");
await t
.expect(Selector("div.p-page-errors").visible)
.notOk()
.expect(Selector("div.p-page-photos").visible)
.ok();
}
);
test.meta("testID", "member-role-006").meta({ type: "smoke" })(
"No private/archived photos in search results",
async (t) => {
await page.login("member", "passwdmember");
if (t.browser.platform === "mobile") {
await t.wait(5000);
}
const PhotoCountBrowse = await photo.getPhotoCount("all");
await toolbar.search("private:true");
const PhotoCountPrivate = await photo.getPhotoCount("all");
await t.expect(PhotoCountPrivate).eql(0);
await photo.checkPhotoVisibility("pqmxlquf9tbc8mk2", false);
await toolbar.search("archived:true");
const PhotoCountArchive = await photo.getPhotoCount("all");
await t.expect(PhotoCountArchive).eql(0);
await photo.checkPhotoVisibility("pqnahct2mvee8sr4", false);
await toolbar.search("quality:0");
if (t.browser.platform === "mobile") {
await t.wait(5000);
}
const PhotoCountReview = await photo.getPhotoCount("all");
await t.expect(PhotoCountReview).gte(PhotoCountBrowse);
await photo.checkPhotoVisibility("pqzuein2pdcg1kc7", true);
await menu.openPage("places");
await t
.expect(Selector("#map").exists, { timeout: 15000 })
.ok()
.expect(Selector("div.p-map-control").visible)
.ok()
.wait(5000);
await t
.typeText(Selector('input[aria-label="Search"]'), "oaxaca", { replace: true })
.pressKey("enter");
await t
.expect(Selector("div.p-map-control").visible)
.ok()
.expect(getLocation())
.contains("oaxaca")
.wait(5000)
.expect(Selector('div[title="Viewpoint / Mexico / 2017"]').visible)
.notOk()
.expect(Selector('div[title="Viewpoint / Mexico / 2018"]').visible)
.notOk();
await t
.typeText(Selector('input[aria-label="Search"]'), "canada", { replace: true })
.pressKey("enter");
await t
.expect(Selector("div.p-map-control").visible)
.ok()
.expect(getLocation())
.contains("canada")
.wait(8000)
.expect(Selector('div[title="Cape / Bowen Island / 2019"]').visible)
.ok()
.expect(Selector('div[title="Truck / Vancouver / 2019"]').visible)
.notOk();
}
);
test.meta("testID", "member-role-007").meta({ type: "smoke" })(
"No upload functionality",
async (t) => {
await page.login("member", "passwdmember");
await toolbar.checkToolbarActionAvailability("upload", false);
await menu.openPage("albums");
await toolbar.checkToolbarActionAvailability("upload", false);
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", false);
await menu.openPage("video");
await toolbar.checkToolbarActionAvailability("upload", false);
await menu.openPage("people");
await toolbar.checkToolbarActionAvailability("upload", false);
await menu.openPage("favorites");
await toolbar.checkToolbarActionAvailability("upload", false);
await menu.openPage("moments");
await toolbar.checkToolbarActionAvailability("upload", false);
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", false);
await menu.openPage("calendar");
await toolbar.checkToolbarActionAvailability("upload", false);
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", false);
await menu.openPage("states");
await toolbar.checkToolbarActionAvailability("upload", false);
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", false);
await menu.openPage("folders");
await toolbar.checkToolbarActionAvailability("upload", false);
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("upload", false);
}
);
test.meta("testID", "member-role-008").meta({ type: "smoke" })(
"Member cannot like photos",
async (t) => {
await page.login("member", "passwdmember");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
const SecondPhotoUid = await photo.getNthPhotoUid("all", 1);
await menu.openPage("favorites");
const FirstFavoriteUid = await photo.getNthPhotoUid("image", 0);
await photo.checkHoverActionState("uid", FirstFavoriteUid, "favorite", true);
await photo.triggerHoverAction("uid", FirstFavoriteUid, "favorite");
await photo.checkHoverActionState("uid", FirstFavoriteUid, "favorite", true);
await photo.checkPhotoVisibility(FirstPhotoUid, false);
await photo.checkPhotoVisibility(SecondPhotoUid, false);
await menu.openPage("browse");
await photoviewer.openPhotoViewer("uid", FirstPhotoUid);
await photoviewer.checkPhotoViewerActionAvailability("like", false);
await photoviewer.checkPhotoViewerActionAvailability("close", true);
await photoviewer.triggerPhotoViewerAction("close");
await t.wait(5000).click(Selector(".p-expand-search", { timeout: 10000 }));
await toolbar.setFilter("view", "Cards");
await photo.checkHoverActionState("uid", FirstPhotoUid, "favorite", false);
await photo.triggerHoverAction("uid", FirstPhotoUid, "favorite");
await photo.checkHoverActionState("uid", FirstPhotoUid, "favorite", false);
await photo.selectPhotoFromUID(SecondPhotoUid);
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.infoTab);
await photoedit.turnSwitchOn("favorite");
await t.click(photoedit.dialogClose);
await contextmenu.clearSelection();
await photo.checkHoverActionState("uid", SecondPhotoUid, "favorite", false);
await menu.openPage("browse");
await toolbar.setFilter("view", "Mosaic");
await photo.checkHoverActionState("uid", FirstPhotoUid, "favorite", false);
await photo.triggerHoverAction("uid", FirstPhotoUid, "favorite");
await photo.checkHoverActionState("uid", FirstPhotoUid, "favorite", false);
await toolbar.setFilter("view", "List");
await photo.checkListViewActionAvailability("like", true);
await photo.triggerListViewActions("nth", 0, "like");
await toolbar.setFilter("view", "Cards");
await photo.checkHoverActionState("uid", FirstPhotoUid, "favorite", false);
await menu.openPage("albums");
await album.openNthAlbum(0);
await toolbar.triggerToolbarAction("list-view");
if (t.browser.platform === "mobile") {
await toolbar.triggerToolbarAction("list-view");
}
await photo.checkListViewActionAvailability("like", true);
}
);
test.meta("testID", "member-role-009").meta({ type: "smoke" })(
"Member cannot private, archive, share, add/remove to album",
async (t) => {
await page.login("member", "passwdmember");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
await photo.selectPhotoFromUID(FirstPhotoUid);
await contextmenu.checkContextMenuActionAvailability("private", false);
await contextmenu.checkContextMenuActionAvailability("archive", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("album", false);
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("edit", true);
await contextmenu.clearSelection();
await toolbar.setFilter("view", "List");
await photo.checkListViewActionAvailability("private", true);
await menu.openPage("albums");
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", false);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("private", false);
await contextmenu.checkContextMenuActionAvailability("remove", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("album", false);
await contextmenu.clearSelection();
await toolbar.triggerToolbarAction("list-view");
if (t.browser.platform === "mobile") {
await toolbar.triggerToolbarAction("list-view");
}
await photo.checkListViewActionAvailability("private", true);
}
);
test.meta("testID", "member-role-010").meta({ type: "smoke" })(
"Member cannot approve low quality photos",
async (t) => {
await page.login("member", "passwdmember");
await toolbar.search('quality:0 name:"photos-013_1"');
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.triggerContextMenuAction("edit", "");
await t.expect(photoedit.detailsApprove.visible).notOk();
}
);
test.meta("testID", "member-role-011").meta({ type: "smoke" })(
"Edit dialog is read only for member",
async (t) => {
await page.login("member", "passwdmember");
await toolbar.search("faces:new");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
await photo.selectPhotoFromUID(FirstPhotoUid);
await contextmenu.triggerContextMenuAction("edit", "");
await photoedit.checkAllDetailsFieldsDisabled(true);
await t.expect(photoedit.detailsApply.visible).notOk();
if (t.browser.platform !== "mobile") {
await t.expect(photoedit.detailsDone.visible).notOk();
}
await t.click(photoedit.labelsTab);
await photoedit.checkFieldDisabled(photoedit.removeLabel, true);
await t
.expect(photoedit.inputLabelName.exists)
.notOk()
.expect(photoedit.addLabel.exists)
.notOk();
await t.click(photoedit.openInlineEdit);
await t.expect(photoedit.inputLabelRename.exists).notOk();
await t.click(photoedit.peopleTab);
await photoedit.checkFieldDisabled(photoedit.inputName, true);
await t.expect(photoedit.removeMarker.exists).notOk();
await t.click(photoedit.infoTab);
await photoedit.checkAllInfoFieldsDisabled(true);
}
);
test.meta("testID", "member-role-012").meta({ type: "smoke" })(
"No edit album functionality",
async (t) => {
await page.login("member", "passwdmember");
await menu.openPage("albums");
await toolbar.checkToolbarActionAvailability("add", false);
await album.checkHoverActionAvailability("nth", 1, "share", false);
const FirstAlbumUid = await album.getNthAlbumUid("all", 0);
await album.selectAlbumFromUID(FirstAlbumUid);
await contextmenu.checkContextMenuActionAvailability("edit", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("clone", false);
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("delete", false);
await contextmenu.clearSelection();
await t.click(page.cardTitle);
await t.expect(albumdialog.description.visible).notOk();
if (await Selector(`a.uid-${FirstAlbumUid}`).hasClass("is-favorite")) {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
} else {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
}
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", false);
await toolbar.checkToolbarActionAvailability("edit", false);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("album", false);
await contextmenu.checkContextMenuActionAvailability("private", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("remove", false);
}
);
test.meta("testID", "member-role-013")("No edit moment functionality", async (t) => {
await page.login("member", "passwdmember");
await menu.openPage("moments");
await album.checkHoverActionAvailability("nth", 0, "share", false);
const FirstAlbumUid = await album.getNthAlbumUid("all", 0);
await album.selectAlbumFromUID(FirstAlbumUid);
await contextmenu.checkContextMenuActionAvailability("edit", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("clone", false);
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("delete", false);
await contextmenu.clearSelection();
await t.click(page.cardTitle);
await t.expect(albumdialog.description.visible).notOk();
if (await Selector(`a.uid-${FirstAlbumUid}`).hasClass("is-favorite")) {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
} else {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
}
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", false);
await toolbar.checkToolbarActionAvailability("edit", false);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("album", false);
await contextmenu.checkContextMenuActionAvailability("private", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("remove", false);
});
test.meta("testID", "member-role-014")("No edit state functionality", async (t) => {
await page.login("member", "passwdmember");
await menu.openPage("states");
await album.checkHoverActionAvailability("nth", 0, "share", false);
const FirstAlbumUid = await album.getNthAlbumUid("all", 0);
await album.selectAlbumFromUID(FirstAlbumUid);
await contextmenu.checkContextMenuActionAvailability("edit", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("clone", false);
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("delete", false);
await contextmenu.clearSelection();
await t.click(page.cardTitle);
await t.expect(albumdialog.description.visible).notOk();
if (await Selector(`a.uid-${FirstAlbumUid}`).hasClass("is-favorite")) {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
} else {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
}
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", false);
await toolbar.checkToolbarActionAvailability("edit", false);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("album", false);
await contextmenu.checkContextMenuActionAvailability("private", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("remove", false);
});
test.meta("testID", "member-role-015")("No edit calendar functionality", async (t) => {
await page.login("member", "passwdmember");
await menu.openPage("calendar");
await album.checkHoverActionAvailability("nth", 0, "share", false);
const FirstAlbumUid = await album.getNthAlbumUid("all", 0);
await album.selectAlbumFromUID(FirstAlbumUid);
await contextmenu.checkContextMenuActionAvailability("edit", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("clone", false);
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("delete", false);
await contextmenu.clearSelection();
await t.click(page.cardTitle);
await t.expect(albumdialog.description.visible).notOk();
if (await Selector(`a.uid-${FirstAlbumUid}`).hasClass("is-favorite")) {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
} else {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
}
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", false);
await toolbar.checkToolbarActionAvailability("edit", false);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("album", false);
await contextmenu.checkContextMenuActionAvailability("private", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("remove", false);
});
test.meta("testID", "member-role-016").meta({ type: "smoke" })(
"No edit folder functionality",
async (t) => {
await page.login("member", "passwdmember");
await menu.openPage("folders");
await album.checkHoverActionAvailability("nth", 0, "share", false);
const FirstAlbumUid = await album.getNthAlbumUid("all", 0);
await album.selectAlbumFromUID(FirstAlbumUid);
await contextmenu.checkContextMenuActionAvailability("edit", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("clone", false);
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("delete", false);
await contextmenu.clearSelection();
await t.click(page.cardTitle);
await t.expect(albumdialog.description.visible).notOk();
if (await Selector(`a.uid-${FirstAlbumUid}`).hasClass("is-favorite")) {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", true);
} else {
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
await album.triggerHoverAction("uid", FirstAlbumUid, "favorite");
await album.checkHoverActionState("uid", FirstAlbumUid, "favorite", false);
}
await album.openNthAlbum(0);
await toolbar.checkToolbarActionAvailability("share", false);
await toolbar.checkToolbarActionAvailability("edit", false);
await photo.toggleSelectNthPhoto(0, "all");
await contextmenu.checkContextMenuActionAvailability("album", false);
await contextmenu.checkContextMenuActionAvailability("private", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("remove", false);
}
);
test.meta("testID", "member-role-017").meta({ type: "smoke" })(
"No edit labels functionality",
async (t) => {
await page.login("member", "passwdmember");
await menu.openPage("labels");
const FirstLabelUid = await label.getNthLabeltUid(0);
await label.checkHoverActionState("uid", FirstLabelUid, "favorite", false);
await label.triggerHoverAction("uid", FirstLabelUid, "favorite");
await label.checkHoverActionState("uid", FirstLabelUid, "favorite", false);
await t.click(Selector(`a.uid-${FirstLabelUid} div.inline-edit`));
await t.expect(photoedit.inputLabelRename.visible).notOk();
await label.selectLabelFromUID(FirstLabelUid);
await contextmenu.checkContextMenuActionAvailability("delete", false);
await contextmenu.checkContextMenuActionAvailability("album", false);
}
);
test.meta("testID", "member-role-018").meta({ type: "smoke" })(
"No unstack, change primary actions",
async (t) => {
await page.login("member", "passwdmember");
await toolbar.search("stack:true");
const FirstPhotoUid = await photo.getNthPhotoUid("image", 0);
await photo.selectPhotoFromUID(FirstPhotoUid);
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.filesTab);
await t
.expect(photoedit.downloadFile.visible)
.ok()
.expect(photoedit.downloadFile.hasAttribute("disabled"))
.notOk();
await t.click(photoedit.toggleExpandFile.nth(1));
await photoedit.checkFieldDisabled(photoedit.downloadFile, false);
await t
.expect(photoedit.downloadFile.visible)
.ok()
.expect(photoedit.unstackFile.visible)
.notOk()
.expect(photoedit.makeFilePrimary.visible)
.notOk()
.expect(photoedit.deleteFile.visible)
.notOk();
}
);
test.meta("testID", "member-role-019").meta({ type: "smoke" })(
"No edit people functionality",
async (t) => {
await page.login("member", "passwdmember");
await menu.openPage("people");
await toolbar.checkToolbarActionAvailability("show-hidden", false);
await toolbar.checkToolbarActionAvailability("upload", false);
await subject.checkSubjectVisibility("name", "Otto Visible", true);
await subject.checkSubjectVisibility("name", "Monika Hide", false);
await t.expect(subject.newTab.exists).notOk();
await t.click(Selector("a div.v-card__title").nth(0));
await t.expect(Selector("div.input-rename input").visible).notOk();
await subject.checkHoverActionAvailability("nth", 0, "hidden", false);
await subject.toggleSelectNthSubject(0);
await contextmenu.checkContextMenuActionAvailability("album", "false");
await contextmenu.checkContextMenuActionAvailability("download", "true");
await contextmenu.clearSelection();
const FirstSubjectUid = subject.getNthSubjectUid(0);
if (await Selector(`a.uid-${FirstSubjectUid}`).hasClass("is-favorite")) {
await subject.checkHoverActionState("uid", FirstSubjectUid, "favorite", true);
await subject.triggerHoverAction("uid", FirstSubjectUid, "favorite");
await subject.checkHoverActionState("uid", FirstSubjectUid, "favorite", true);
} else {
await subject.checkHoverActionState("uid", FirstSubjectUid, "favorite", false);
await subject.triggerHoverAction("uid", FirstSubjectUid, "favorite");
await subject.checkHoverActionState("uid", FirstSubjectUid, "favorite", false);
}
await subject.openNthSubject(0);
await photo.toggleSelectNthPhoto(0);
await contextmenu.triggerContextMenuAction("edit", "");
await t.click(photoedit.peopleTab);
await photoedit.checkFieldDisabled(photoedit.inputName);
await t.expect(photoedit.rejectName.hasClass("v-icon--disabled")).ok();
await t.navigateTo("/people/new");
await t.expect(Selector("div.is-face").visible).notOk().expect(subject.newTab.exists).notOk();
await t.navigateTo("/people?hidden=yes&order=relevance");
await t.expect(Selector("a.is-subject").visible).notOk();
}
);

View file

@ -1,194 +1,192 @@
import { Selector } from "testcafe";
import { Role } from "testcafe";
import testcafeconfig from "../acceptance/testcafeconfig";
import Page from "../acceptance/page-model";
import Page from "../page-model/page";
import Menu from "../page-model/menu";
import Toolbar from "../page-model/toolbar";
import ContextMenu from "../page-model/context-menu";
import Album from "../page-model/album";
import PhotoViewer from "../page-model/photoviewer";
import ShareDialog from "../page-model/dialog-share";
import Photo from "../page-model/photo";
fixture`Test link sharing`.page`${testcafeconfig.url}`;
fixture`Test link sharing`.page`${testcafeconfig.url}`.skip("Urls are not working anymore");
const page = new Page();
const menu = new Menu();
const toolbar = new Toolbar();
const contextmenu = new ContextMenu();
const album = new Album();
const photoviewer = new PhotoViewer();
const sharedialog = new ShareDialog();
const photo = new Photo();
test.meta("testID", "authentication-000")(
//TODO merge with other sharing test
test.skip.meta("testID", "authentication-000")(
"Time to start instance (will be marked as unstable)",
async (t) => {
await t.wait(5000);
}
);
test.meta("testID", "sharing-001")("View shared albums", async (t) => {
test.skip.meta("testID", "sharing-001")("View shared albums", async (t) => {
await page.login("admin", "photoprism");
await page.openNav();
await t.click(Selector(".nav-albums"));
const FirstAlbum = await Selector("a.is-album").nth(0).getAttribute("data-uid");
await page.selectFromUID(FirstAlbum);
const clipboardCount = await Selector("span.count-clipboard");
await menu.openPage("albums");
const FirstAlbumUid = await album.getNthAlbumUid("all", 0);
await album.triggerHoverAction("uid", FirstAlbumUid, "select");
await contextmenu.checkContextMenuCount("1");
await contextmenu.triggerContextMenuAction("share", "");
await t.click(sharedialog.expandLink.nth(0));
await t
.expect(clipboardCount.textContent)
.eql("1")
.click(Selector("button.action-menu"))
.click(Selector("button.action-share"))
.click(Selector("div.v-expansion-panel__header__icon").nth(0));
await t
.typeText(Selector(".input-secret input"), "secretForTesting", { replace: true })
.click(Selector(".input-expires input"))
.typeText(sharedialog.linkSecretInput, "secretForTesting", { replace: true })
.click(sharedialog.linkExpireInput)
.click(Selector("div").withText("After 1 day").parent('div[role="listitem"]'))
.click(Selector("button.action-save"));
const Url = await Selector("div.input-url input").value;
.click(sharedialog.dialogSave);
const Url = await sharedialog.linkUrl.value;
const Expire = await Selector("div.v-select__selections").innerText;
await t.expect(Url).contains("secretfortesting").expect(Expire).contains("After 1 day");
let url = Url.replace("2342", "2343");
await t.click(Selector("button.action-close"));
await page.clearSelection();
await t.click(Selector(".nav-folders"));
const FirstFolder = await Selector("a.is-album").nth(0).getAttribute("data-uid");
await page.selectFromUID(FirstFolder);
await t.click(sharedialog.dialogClose);
await contextmenu.clearSelection();
await menu.openPage("folders");
const FirstFolderUid = await album.getNthAlbumUid("all", 0);
await album.triggerHoverAction("uid", FirstFolderUid, "select");
await contextmenu.checkContextMenuCount("1");
await contextmenu.triggerContextMenuAction("share", "");
await t.click(sharedialog.expandLink.nth(0));
await t
.click(Selector("button.action-menu"))
.click(Selector("button.action-share"))
.click(Selector("div.v-expansion-panel__header__icon").nth(0));
await t
.typeText(Selector(".input-secret input"), "secretForTesting", { replace: true })
.click(Selector(".input-expires input"))
.typeText(sharedialog.linkSecretInput, "secretForTesting", { replace: true })
.click(sharedialog.linkExpireInput)
.click(Selector("div").withText("After 1 day").parent('div[role="listitem"]'))
.click(Selector("button.action-save"))
.click(Selector("button.action-close"));
await page.clearSelection();
.click(sharedialog.dialogSave)
.click(sharedialog.dialogSave);
await contextmenu.clearSelection();
await t.navigateTo(url);
await t
.expect(Selector("div.v-toolbar__title").withText("Christmas").visible)
.ok()
.click(Selector("button").withText("@photoprism_app"))
.expect(Selector("div.v-toolbar__title").withText("Albums").visible)
.ok();
const countAlbums = await Selector("a.is-album").count;
await t.expect(countAlbums).gte(40).useRole(Role.anonymous());
await t.expect(toolbar.toolbarTitle.withText("Christmas").visible).ok();
await t.click(Selector("button").withText("@photoprism_app"));
await t.expect(toolbar.toolbarTitle.withText("Albums").visible).ok();
const AlbumCount = await album.getAlbumCount("all");
await t.expect(AlbumCount).gte(40);
await t.useRole(Role.anonymous());
await t.navigateTo(url);
await t
.expect(Selector("div.v-toolbar__title").withText("Christmas").visible)
.ok()
.click(Selector("button").withText("@photoprism_app"))
.expect(Selector("div.v-toolbar__title").withText("Albums").visible)
.ok();
const countAlbumsAnonymous = await Selector("a.is-album").count;
await t.expect(countAlbumsAnonymous).eql(2);
await t.expect(toolbar.toolbarTitle.withText("Christmas").visible).ok();
await t.click(Selector("button").withText("@photoprism_app"));
await t.expect(toolbar.toolbarTitle.withText("Albums").visible).ok();
const AlbumCountAnonymous = await Selector("a.is-album").count;
await t.expect(AlbumCountAnonymous).eql(2);
await t.navigateTo("http://localhost:2343/browse");
await page.login("admin", "photoprism");
await page.openNav();
await menu.openPage("albums");
await album.openAlbumWithUid(FirstAlbumUid);
await toolbar.triggerToolbarAction("share");
await t
.click(Selector(".nav-albums"))
.click(Selector("a.is-album").withAttribute("data-uid", FirstAlbum))
.click(Selector("button.action-share"))
.click(Selector("div.v-expansion-panel__header__icon").nth(0))
.click(Selector(".action-delete"))
.useRole(Role.anonymous())
.expect(Selector(".input-name input").visible)
.ok();
.click(sharedialog.expandLink.nth(0))
.click(sharedialog.deleteLink)
.useRole(Role.anonymous());
await t.expect(Selector(".input-name input").visible).ok();
await t.navigateTo("http://localhost:2343/s/secretfortesting");
await t.expect(Selector("div.v-toolbar__title").withText("Albums").visible).ok();
const countAlbumsAnonymousAfterDelete = await Selector("a.is-album").count;
await t.expect(countAlbumsAnonymousAfterDelete).eql(1);
await t.expect(toolbar.toolbarTitle.withText("Albums").visible).ok();
const AlbumCountAnonymousAfterDelete = await album.getAlbumCount("all");
await t.expect(AlbumCountAnonymousAfterDelete).eql(1);
await t.navigateTo("http://localhost:2343/browse");
await page.login("admin", "photoprism");
await page.openNav();
await menu.openPage("folders");
await album.openAlbumWithUid(FirstFolderUid);
await toolbar.triggerToolbarAction("share");
await t
.click(Selector(".nav-folders"))
.click(Selector("a.is-album").withAttribute("data-uid", FirstFolder))
.click(Selector("button.action-share"))
.click(Selector("div.v-expansion-panel__header__icon").nth(0))
.click(Selector(".action-delete"))
.useRole(Role.anonymous())
.expect(Selector(".input-name input").visible)
.ok();
.click(sharedialog.expandLink.nth(0))
.click(sharedialog.deleteLink)
.useRole(Role.anonymous());
await t.expect(Selector(".input-name input").visible).ok();
await t.navigateTo("http://localhost:2343/s/secretfortesting");
await t
.expect(Selector("div.v-toolbar__title").withText("Christmas").visible)
.expect(toolbar.toolbarTitle.withText("Christmas").visible)
.notOk()
.expect(Selector("div.v-toolbar__title").withText("Albums").visible)
.expect(toolbar.toolbarTitle.withText("Albums").visible)
.notOk()
.expect(Selector(".input-name input").visible)
.ok();
});
test.meta("testID", "sharing-002")("Verify anonymous user has limited options", async (t) => {
test.skip.meta("testID", "sharing-002")("Verify anonymous user has limited options", async (t) => {
await t.navigateTo("http://localhost:2343/s/jxoux5ub1e/british-columbia-canada");
// check album toolbar
await t.expect(toolbar.toolbarTitle.withText("British Columbia").visible).ok();
await toolbar.checkToolbarActionAvailability("edit", false);
await toolbar.checkToolbarActionAvailability("share", false);
await toolbar.checkToolbarActionAvailability("upload", false);
await toolbar.checkToolbarActionAvailability("reload", true);
await toolbar.checkToolbarActionAvailability("download", true);
await photo.triggerHoverAction("nth", 0, "select");
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("archive", false);
await contextmenu.checkContextMenuActionAvailability("private", false);
await contextmenu.checkContextMenuActionAvailability("edit", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.checkContextMenuActionAvailability("album", false);
await contextmenu.clearSelection();
await t.expect(page.cardTitle.visible).notOk();
await photoviewer.openPhotoViewer("nth", 0);
await photoviewer.checkPhotoViewerActionAvailability("download", true);
await photoviewer.checkPhotoViewerActionAvailability("select", true);
await photoviewer.checkPhotoViewerActionAvailability("fullscreen", true);
await photoviewer.checkPhotoViewerActionAvailability("slideshow", true);
await photoviewer.checkPhotoViewerActionAvailability("like", false);
await photoviewer.checkPhotoViewerActionAvailability("edit", false);
await photoviewer.triggerPhotoViewerAction("close");
await photo.checkHoverActionAvailability("nth", 0, "favorite", false);
await photo.checkHoverActionAvailability("nth", 0, "select", true);
await toolbar.triggerToolbarAction("view-list");
await t
.expect(Selector("div.v-toolbar__title").withText("British Columbia").visible)
.ok()
.expect(Selector("button.action-edit").visible)
.expect(Selector(`td button.input-private`).visible)
.notOk()
.expect(Selector("button.action-share").visible)
.expect(Selector(`td button.input-favorite`).visible)
.notOk()
.expect(Selector("button.action-upload").visible)
.notOk()
.expect(Selector("button.action-reload").visible)
.ok()
.expect(Selector("button.action-download").visible)
.ok();
//check photo context menu
await page.toggleSelectNthPhoto(0);
await t
.click("button.action-menu")
.expect(Selector("div.v-speed-dial__list button.action-download").visible)
.ok()
.expect(Selector("div.v-speed-dial__list button.action-archive").visible)
.notOk()
.expect(Selector("div.v-speed-dial__list button.action-album").visible)
.notOk()
.expect(Selector("div.v-speed-dial__list button.action-private").visible)
.notOk()
.expect(Selector("div.v-speed-dial__list button.action-edit").visible)
.notOk()
.expect(Selector("div.v-speed-dial__list button.action-share").visible)
.notOk();
await page.clearSelection();
await t.expect(Selector("button.action-title-edit").visible).notOk();
//check fullscreen actions
await t
.click(Selector('h3[title="Cape / Bowen Island / 2019"]'))
.expect(Selector("#photo-viewer").visible)
.ok()
.expect(Selector("img.pswp__img").visible)
.ok()
.expect(Selector("button.action-select").visible)
.ok()
.expect(Selector('button[title="Start/Stop Slideshow"]').visible)
.ok()
.expect(Selector('button[title="Fullscreen"]').visible)
.ok()
.expect(Selector('button[title="Start/Stop Slideshow"]').visible)
.ok()
.expect(Selector('button[title="Download"]').visible)
.ok()
.expect(Selector('button[title="Like"]').visible)
.notOk()
.expect(Selector('button[title="Edit"]').visible)
.notOk()
.click(Selector('button[title="Close"]'))
//check hover like actions card and mosaic
.expect(Selector("button.input-favorite").visible)
.notOk()
//check list view actions
//hover on mosaic
//action-menu albums
.click(Selector("button").withText("@photoprism_app"))
.expect(Selector("div.v-toolbar__title").withText("Albums").visible)
.expect(toolbar.toolbarTitle.withText("Albums").visible)
.ok();
//album edit dialog
const AlbumUid = await Selector("a.is-album", { timeout: 55000 }).nth(0).getAttribute("data-uid");
await page.selectFromUID(AlbumUid);
await t
.click(Selector("button.action-menu"))
.expect(Selector("div.v-speed-dial__list button.action-download").visible)
.ok()
.expect(Selector("div.v-speed-dial__list button.action-delete").visible)
.notOk()
.expect(Selector("div.v-speed-dial__list button.action-album").visible)
.notOk()
.expect(Selector("div.v-speed-dial__list button.action-edit").visible)
.notOk()
.expect(Selector("div.v-speed-dial__list button.action-share").visible)
.notOk();
await page.clearSelection();
await t.expect(Selector("button.action-title-edit").visible).notOk();
//TODO control + page model
const AlbumUid = await album.getNthAlbumUid("all", 0);
await album.triggerHoverAction("uid", AlbumUid, "select");
await contextmenu.checkContextMenuActionAvailability("download", true);
await contextmenu.checkContextMenuActionAvailability("delete", false);
await contextmenu.checkContextMenuActionAvailability("album", false);
await contextmenu.checkContextMenuActionAvailability("edit", false);
await contextmenu.checkContextMenuActionAvailability("share", false);
await contextmenu.clearSelection();
});

View file

@ -170,8 +170,6 @@ test.meta("testID", "albums-007")("Create/delete album during add to album", asy
});
test.meta("testID", "albums-008")("Test album autocomplete", async (t) => {
await page.openNav();
await t.click(Selector(".nav-browse"));
await page.search("photo:true");
const FirstPhotoUid = await Selector("div.is-photo.type-image").nth(0).getAttribute("data-uid");
await page.selectPhotoFromUID(FirstPhotoUid);

View file

@ -660,4 +660,129 @@ export default class Page {
await t.click(Selector("button.action-done", { timeout: 5000 }));
}
}
async checkMemberAlbumRights(type) {
await t.expect(Selector("a.is-album button.action-share").visible).notOk();
const FirstAlbum = await Selector("a.is-album").nth(0).getAttribute("data-uid");
await this.selectFromUID(FirstAlbum);
await t
.click(Selector("button.action-menu"))
.expect(Selector("button.action-edit").visible)
.notOk()
.expect(Selector("button.action-share").visible)
.notOk()
.expect(Selector("button.action-clone").visible)
.notOk()
.expect(Selector("button.action-download").visible)
.ok();
if (type == "album" || type == "moment" || type == "state") {
await t.expect(Selector("button.action-delete").visible).notOk();
}
await this.clearSelection();
await t
.click(Selector("button.action-title-edit"))
.expect(Selector(".input-description textarea").visible)
.notOk();
if (await Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite")) {
await t
.expect(Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite"))
.ok()
.click(Selector(`.uid-${FirstAlbum} .input-favorite`))
.expect(Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite"))
.ok();
} else {
await t
.expect(Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite"))
.notOk()
.click(Selector(`.uid-${FirstAlbum} .input-favorite`))
.expect(Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite"))
.notOk();
}
await t
.click(Selector("a.is-album").nth(0))
.expect(Selector("button.action-share").visible)
.notOk()
.expect(Selector("button.action-edit").visible)
.notOk();
await this.toggleSelectNthPhoto(0);
await t.click(Selector("button.action-menu"));
if (type == "album") {
await t.expect(Selector("button.action-remove").visible).notOk();
} else {
await t.expect(Selector("button.action-archive").visible).notOk();
}
await t
.expect(Selector("button.action-album").visible)
.notOk()
.expect(Selector("button.action-private").visible)
.notOk()
.expect(Selector("button.action-share").visible)
.notOk();
}
async checkAdminAlbumRights(type) {
await t.expect(Selector("a.is-album button.action-share").visible).ok();
const FirstAlbum = await Selector("a.is-album").nth(0).getAttribute("data-uid");
await this.selectFromUID(FirstAlbum);
await t
.click(Selector("button.action-menu"))
.expect(Selector("button.action-edit").visible)
.ok()
.expect(Selector("button.action-share").visible)
.ok()
.expect(Selector("button.action-clone").visible)
.ok()
.expect(Selector("button.action-download").visible)
.ok();
if (type == "album" || type == "moment" || type == "state") {
await t.expect(Selector("button.action-delete").visible).ok();
}
await this.clearSelection();
await t
.click(Selector("button.action-title-edit"))
.expect(Selector(".input-description textarea").visible)
.ok()
.click(Selector("button.action-cancel"));
if (await Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite")) {
await t
.expect(Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite"))
.ok()
.click(Selector(`.uid-${FirstAlbum} .input-favorite`))
.expect(Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite"))
.notOk()
.click(Selector(`.uid-${FirstAlbum} .input-favorite`))
.expect(Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite"))
.ok();
} else {
await t
.expect(Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite"))
.notOk()
.click(Selector(`.uid-${FirstAlbum} .input-favorite`))
.expect(Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite"))
.ok()
.click(Selector(`.uid-${FirstAlbum} .input-favorite`))
.expect(Selector(`a.uid-${FirstAlbum}`).hasClass("is-favorite"))
.notOk();
}
await t
.click(Selector("a.is-album").nth(0))
.expect(Selector("button.action-share").visible)
.ok()
.expect(Selector("button.action-edit").visible)
.ok();
await this.toggleSelectNthPhoto(0);
await t.click(Selector("button.action-menu"));
if (type == "album") {
await t.expect(Selector("button.action-remove").visible).ok();
} else {
await t.expect(Selector("button.action-archive").visible).ok();
}
await t
.expect(Selector("button.action-album").visible)
.ok()
.expect(Selector("button.action-private").visible)
.ok()
.expect(Selector("button.action-share").visible)
.ok();
}
}

View file

@ -9,8 +9,6 @@ const page = new Page();
test.meta("testID", "photos-005")(
"Private/unprivate photo/video using clipboard and list",
async (t) => {
await page.openNav();
await t.click(Selector(".nav-browse"));
await page.search("photo:true");
await page.setFilter("view", "Mosaic");
const FirstPhoto = await Selector("div.is-photo").nth(0).getAttribute("data-uid");
@ -168,8 +166,6 @@ test.meta("testID", "photos-005")(
test.meta("testID", "photos-006")(
"Archive/restore video, photos, private photos and review photos using clipboard",
async (t) => {
await page.openNav();
await t.click(Selector(".nav-browse"));
await page.search("photo:true");
await page.setFilter("view", "Mosaic");
const FirstPhoto = await Selector("div.is-photo").nth(0).getAttribute("data-uid");

View file

@ -41,25 +41,20 @@ test.meta("testID", "photos-download-001")(
}
);
test.meta("testID", "photos-download-002")(
"Test download video from context menu",
async (t) => {
await page.openNav();
await t.click(Selector("div.nav-browse"));
await page.search("name:Mohn.mp4");
const Photo = await Selector("div.is-photo").nth(0).getAttribute("data-uid");
await page.selectPhotoFromUID(Photo);
await t.click(Selector("button.action-menu"));
await logger.clear();
await t.click(Selector("button.action-download"));
const requestInfo = await logger.requests[1].response;
const requestInfo2 = await logger.requests[2].response;
await page.validateDownloadRequest(requestInfo, "Mohn", ".mp4.jpg");
await page.validateDownloadRequest(requestInfo2, "Mohn", ".mp4");
await logger.clear();
await page.clearSelection();
}
);
test.meta("testID", "photos-download-002")("Test download video from context menu", async (t) => {
await page.search("name:Mohn.mp4");
const Photo = await Selector("div.is-photo").nth(0).getAttribute("data-uid");
await page.selectPhotoFromUID(Photo);
await t.click(Selector("button.action-menu"));
await logger.clear();
await t.click(Selector("button.action-download"));
const requestInfo = await logger.requests[1].response;
const requestInfo2 = await logger.requests[2].response;
await page.validateDownloadRequest(requestInfo, "Mohn", ".mp4.jpg");
await page.validateDownloadRequest(requestInfo2, "Mohn", ".mp4");
await logger.clear();
await page.clearSelection();
});
test.meta("testID", "photos-download-003")(
"Test download multiple jpg files from context menu",
@ -85,8 +80,6 @@ test.meta("testID", "photos-download-003")(
test.meta("testID", "photos-download-004")(
"Test raw file from context menu and fullscreen mode",
async (t) => {
await page.openNav();
await t.click(Selector("div.nav-browse"));
await page.search("name:elephantRAW");
const Photo = await Selector("div.is-photo").nth(0).getAttribute("data-uid");
await page.selectPhotoFromUID(Photo);

View file

@ -10,8 +10,6 @@ const page = new Page();
test.meta("testID", "photos-upload-delete-001")("Upload + Delete jpg/json", async (t) => {
await t.expect(fs.existsSync("../storage/acceptance/originals/2020/10")).notOk();
await page.openNav();
await t.click(Selector(".nav-browse"));
await page.search("digikam");
const PhotoCount = await Selector("div.is-photo").count;
await t
@ -42,9 +40,11 @@ test.meta("testID", "photos-upload-delete-001")("Upload + Delete jpg/json", asyn
.ok()
.click(Selector(".action-close"));
await page.clearSelection();
await t.expect(fs.existsSync("../storage/acceptance/originals/2020/10")).ok();
const originalsLength = fs.readdirSync("../storage/acceptance/originals/2020/10").length;
await t.expect(originalsLength).eql(2);
if (t.browser.platform !== "mobile") {
await t.expect(fs.existsSync("../storage/acceptance/originals/2020/10")).ok();
const originalsLength = fs.readdirSync("../storage/acceptance/originals/2020/10").length;
await t.expect(originalsLength).eql(2);
}
await page.deletePhotoFromUID(UploadedPhoto);
await page.openNav();
await t.click(Selector(".nav-browse"));
@ -55,24 +55,23 @@ test.meta("testID", "photos-upload-delete-001")("Upload + Delete jpg/json", asyn
.navigateTo("/library/files/2020/10");
const FileCountAfterDelete = await Selector("div.is-file").count;
await t.expect(FileCountAfterDelete).eql(0);
const originalsLengthAfterDelete = fs.readdirSync("../storage/acceptance/originals/2020/10")
.length;
await t.expect(originalsLengthAfterDelete).eql(0);
if (t.browser.platform !== "mobile") {
const originalsLengthAfterDelete = fs.readdirSync(
"../storage/acceptance/originals/2020/10"
).length;
await t.expect(originalsLengthAfterDelete).eql(0);
}
});
test.meta("testID", "photos-upload-delete-002")("Upload + Delete video", async (t) => {
await t.expect(fs.existsSync("../storage/acceptance/originals/2020/06")).notOk();
await page.openNav();
await t.click(Selector(".nav-browse"));
await page.search("korn");
const PhotoCount = await Selector("div.is-photo").count;
await t
.expect(PhotoCount)
.eql(0)
.click(Selector(".action-upload"))
.setFilesToUpload(Selector(".input-upload"), [
"./upload-files/korn.mp4",
])
.setFilesToUpload(Selector(".input-upload"), ["./upload-files/korn.mp4"])
.wait(15000);
const PhotoCountAfterUpload = await Selector("div.is-photo").count;
await t.expect(PhotoCountAfterUpload).eql(1);
@ -93,11 +92,13 @@ test.meta("testID", "photos-upload-delete-002")("Upload + Delete video", async (
.ok()
.click(Selector(".action-close"));
await page.clearSelection();
await t.expect(fs.existsSync("../storage/acceptance/originals/2020/06")).ok();
const originalsLength = fs.readdirSync("../storage/acceptance/originals/2020/06").length;
await t.expect(originalsLength).eql(1);
const sidecarLength = fs.readdirSync("../storage/acceptance/originals/2020/06").length;
await t.expect(sidecarLength).eql(1);
if (t.browser.platform !== "mobile") {
await t.expect(fs.existsSync("../storage/acceptance/originals/2020/06")).ok();
const originalsLength = fs.readdirSync("../storage/acceptance/originals/2020/06").length;
await t.expect(originalsLength).eql(1);
const sidecarLength = fs.readdirSync("../storage/acceptance/originals/2020/06").length;
await t.expect(sidecarLength).eql(1);
}
await page.deletePhotoFromUID(UploadedPhoto);
await page.openNav();
await t.click(Selector(".nav-browse"));
@ -108,11 +109,16 @@ test.meta("testID", "photos-upload-delete-002")("Upload + Delete video", async (
.navigateTo("/library/files/2020/06");
const FileCountAfterDelete = await Selector("div.is-file").count;
await t.expect(FileCountAfterDelete).eql(0);
const originalsLengthAfterDelete = fs.readdirSync("../storage/acceptance/originals/2020/06")
.length;
await t.expect(originalsLengthAfterDelete).eql(0);
const sidecarLengthAfterDelete = fs.readdirSync("../storage/acceptance/originals/2020/06").length;
await t.expect(sidecarLengthAfterDelete).eql(0);
if (t.browser.platform !== "mobile") {
const originalsLengthAfterDelete = fs.readdirSync(
"../storage/acceptance/originals/2020/06"
).length;
await t.expect(originalsLengthAfterDelete).eql(0);
const sidecarLengthAfterDelete = fs.readdirSync(
"../storage/acceptance/originals/2020/06"
).length;
await t.expect(sidecarLengthAfterDelete).eql(0);
}
});
test.meta("testID", "photos-upload-delete-003")("Upload to existing Album + Delete", async (t) => {
@ -201,8 +207,6 @@ test.meta("testID", "photos-upload-delete-004")("Upload jpg to new Album + Delet
});
test.meta("testID", "photos-upload-delete-005")("Try uploading nsfw file", async (t) => {
await page.openNav();
await t.click(Selector(".nav-browse"));
await t
.click(Selector(".action-upload"))
.setFilesToUpload(Selector(".input-upload"), ["./upload-files/hentai_2.jpg"])
@ -216,8 +220,6 @@ test.meta("testID", "photos-upload-delete-005")("Try uploading nsfw file", async
});
test.meta("testID", "photos-upload-delete-006")("Try uploading txt file", async (t) => {
await page.openNav();
await t.click(Selector(".nav-browse"));
await t
.click(Selector(".action-upload"))
.setFilesToUpload(Selector(".input-upload"), ["./upload-files/foo.txt"])

Some files were not shown because too many files have changed in this diff Show more