Compare commits

...
Author SHA1 Message Date
rmmayo 1925c31934 upgrade spring-boot to 4.1.2 2026-10-23 14:13:44 +00:00
Ryan Mayo 48ff8e052c Merge pull request #291 from NationalSecurityAgency/tests_fix
Tests fix
2026-10-08 08:14:07 -04:00
Dima May 037009f8a8 Fixed OAuth Cypress login by probing session instead of project token 2026-10-08 10:29:31 +00:00
Dima May dca695d685 Increased maximum log requests during a one-minute window 2026-10-07 18:28:00 +00:00
Dima May 24905e9a42 Fix cypress tests client CSRF handling and backed user creation process 2026-10-05 10:28:14 +00:00
Dima May 5a98e7fd2c creating_release_version_3.6.5 2026-10-02 11:43:21 +00:00
Ryan Mayo 897d1b9b1c Merge pull request #290 from NationalSecurityAgency/int_client_session
Fix integration client session and CSRF handling for login
2026-09-29 09:04:35 -04:00
Dima May 3a5bcc1f04 Fix integration client session and CSRF handling for login 2026-09-29 11:12:02 +00:00
Ryan Mayo 030a8bd24a Merge pull request #289 from NationalSecurityAgency/xhr_withCredentials
Xhr with credentials
2026-09-23 15:19:21 -04:00
Dima May 532f33a8a7 fixed tests 2026-09-23 15:45:29 +00:00
Dima May b070e4058e only set xhr.withCredentials to true in PKI mode 2026-09-23 14:39:08 +00:00
Dima May 6b1ba358d8 Merge pull request #288 from NationalSecurityAgency/remove_scheuduled_ci_actions
updated skills-client CI actions to no longer run on a schedule
2026-09-15 11:46:54 -04:00
rmmayo e845fb7788 updated skills-client CI actions to no longer run on a schedule 2026-09-15 15:28:24 +00:00
rmmayo 599e838086 updated skills-client-js to add the current window.location.pathname as the default parentPath option 2026-09-15 15:11:02 +00:00
rmmayo ad5ab89439 creating_release_version_3.6.4 2026-09-14 19:38:41 +00:00
rmmayo 3c408f555f updated the dependencies 2026-09-14 19:11:24 +00:00
rmmayo ea523e9381 updated the versions for testing 2026-09-14 13:59:38 +00:00
rmmayo cea08a3dd5 set parentPath to window.location.pathname so that it is available to the skills-display iframe 2026-09-09 21:56:16 +00:00
rmmayo 731853a8a6 globalEventResult can be empty string or empty JSON object (after spring-boot 4 upgrade) 2026-08-26 16:46:03 +00:00
rmmayo 78c338fd36 updated spring-boot 2026-08-26 16:45:12 +00:00
rmmayo 80052f77ac remove unnecessary spring.autoconfigure.exclude props from tests since it's the default in the main application.yml 2026-08-26 16:45:00 +00:00
rmmayo b4e7718741 Updated the regex to match the completed array when it contains an object with:
- `"type": "Overall"`
- `"level": 1`

regardless of property order.
2026-08-26 14:59:35 +00:00
rmmayo 42c40ce92f Updated the regex to match the completed array when it contains an object with:
- `"type": "Overall"`
- `"level": 1`

regardless of property order.
2026-08-26 12:03:02 +00:00
Ryan Mayo 18ad1af38e Merge pull request #287 from NationalSecurityAgency/spring_4_upgrade
upgraded to spring boot 4.1.0
2026-08-12 16:11:07 -04:00
Dima May 58f6265836 upgraded to spring boot 4.1.0 2026-08-12 19:35:43 +00:00
Ryan Mayo e3f299376c Merge pull request #285 from arpitjain099/fix/encode-oauth-redirect-uri
Encode the redirect target before interpolating it into the authenticator URL
2026-07-31 10:09:11 -04:00
Arpit Jain 2b66377a30 Encode the redirect target before interpolating it into the authenticator URL
getAuthenticationToken built the OAuth redirect by interpolating
window.location straight into the query string, so any reserved
character already present in the current URL was carried through as
structure rather than data. A page reached with an ampersand in its
query string could therefore append parameters to the authenticator
URL, including a second skillsRedirectUri.

Use encodeURIComponent(window.location.href) so the value is passed as
a single opaque parameter. Closes #284.

Signed-off-by: Arpit Jain <[email protected]>
2026-07-31 00:06:02 +09:00
Dima May b88c19d0c7 Merge pull request #283 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-integration/skills-int-client-js/axios-1.18.0
Bump axios from 1.17.0 to 1.18.0 in /skills-client-integration/skills-int-client-js
2026-07-21 07:21:08 -04:00
dependabot[bot] 558cecacdd Bump axios in /skills-client-integration/skills-int-client-js
Bumps [axios](https://github.com/axios/axios) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-07-21 10:55:51 +00:00
Dima May dbcc95f951 Merge pull request #282 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-js/axios-1.18.0
Bump axios from 1.17.0 to 1.18.0 in /skills-client-js
2026-07-21 06:54:48 -04:00
dependabot[bot] 50ac5c413f Bump axios from 1.17.0 to 1.18.0 in /skills-client-js
Bumps [axios](https://github.com/axios/axios) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.17.0...v1.18.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-07-20 20:13:33 +00:00
Don Walizer cf0961c435 Merge pull request #280 from NationalSecurityAgency/upgrades_gh-actions_node_java
upgrade gh actions, node and java
2026-07-06 09:38:03 -04:00
Dima May 56cb884fdb Merge pull request #279 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-js/babel/core-7.29.6
Bump @babel/core from 7.29.0 to 7.29.6 in /skills-client-js
2026-07-06 08:02:34 -04:00
Dima May 6af2944841 upgrade gh actions, node and java 2026-07-06 11:49:49 +00:00
dependabot[bot] a301650700 Bump @babel/core from 7.29.0 to 7.29.6 in /skills-client-js
Bumps [@babel/core](https://github.com/babel/babel/tree/HEAD/packages/babel-core) from 7.29.0 to 7.29.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.29.6/packages/babel-core)

---
updated-dependencies:
- dependency-name: "@babel/core"
  dependency-version: 7.29.6
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-06-25 14:51:08 +00:00
rmmayo d19947c814 upgraded spring boot to v3.5.16 2026-06-25 14:49:46 +00:00
Dima May 361bae4289 spring boot upgrade; set license-maven-plugin to use the latest version 2026-06-11 14:50:16 +00:00
Dima May aae9cc7ee9 Merge pull request #278 from NationalSecurityAgency/t#277/upgrade_axios
#277 - upgrade axios
2026-06-08 15:24:47 -04:00
rmmayo 60c3d5edb1 #277 - upgrade axios 2026-06-08 14:48:22 +00:00
Dima May bb12395d80 creating_release_version_3.6.3 2026-05-12 17:39:20 +00:00
Ryan Mayo dea819670b Merge pull request #276 from NationalSecurityAgency/t#247/readme
T#247/readme
2026-05-12 13:24:29 -04:00
Dima May 7553782dcc #247: added badges 2026-05-12 17:04:39 +00:00
Dima May daf5b1fca8 #247: updated readme 2026-05-12 17:02:15 +00:00
Ryan Mayo 83878d8c3a Merge pull request #275 from NationalSecurityAgency/t#274/upgrade_libs
T#274/upgrade libs
2026-05-12 11:01:53 -04:00
Dima May 83178bf58b #247: upgraded JS libraries used in end-to-end tests 2026-05-12 14:33:26 +00:00
Dima May 03cf175e26 #247: upgraded JS libraries, rewrote tests due to jdom upgrade; new tests to improve test coverage 2026-05-12 14:14:33 +00:00
Dima May 04c9f2e950 Merge pull request #272 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-integration/skills-int-client-js/axios-1.15.2
Bump axios from 1.15.0 to 1.15.2 in /skills-client-integration/skills-int-client-js
2026-05-08 14:07:55 +00:00
Dima May 3f952ba966 Merge pull request #273 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-js/axios-1.15.2
Bump axios from 1.15.0 to 1.15.2 in /skills-client-js
2026-05-08 14:07:35 +00:00
dependabot[bot] b7ad03fb64 Bump axios from 1.15.0 to 1.15.2 in /skills-client-js
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-05-08 06:29:38 +00:00
dependabot[bot] 39c7abc757 Bump axios in /skills-client-integration/skills-int-client-js
Bumps [axios](https://github.com/axios/axios) from 1.15.0 to 1.15.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.15.0...v1.15.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-05-08 06:15:00 +00:00
rmmayo aa0af7e776 upgraded spring-boot to v3.5.14 2026-04-23 18:05:13 +00:00
Dima May 33cfff5ebb Merge pull request #270 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-integration/skills-int-client-js/axios-1.15.0
Bump axios from 1.13.5 to 1.15.0 in /skills-client-integration/skills-int-client-js
2026-04-16 08:33:58 -04:00
Dima May 7efe886ef4 Merge pull request #271 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-js/axios-1.15.0
Bump axios from 1.13.5 to 1.15.0 in /skills-client-js
2026-04-16 08:33:32 -04:00
dependabot[bot] caa236dff1 Bump axios from 1.13.5 to 1.15.0 in /skills-client-js
Bumps [axios](https://github.com/axios/axios) from 1.13.5 to 1.15.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.5...v1.15.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-13 07:48:39 +00:00
dependabot[bot] 2b5b28c8dc Bump axios in /skills-client-integration/skills-int-client-js
Bumps [axios](https://github.com/axios/axios) from 1.13.5 to 1.15.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.13.5...v1.15.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.15.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-12 10:27:26 +00:00
Dima May 481a6d0505 Merge pull request #269 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-integration/skills-int-client-js/axios-1.13.5
Bump axios from 1.12.0 to 1.13.5 in /skills-client-integration/skills-int-client-js
2026-04-06 14:03:15 -04:00
dependabot[bot] 5103989eef Bump axios in /skills-client-integration/skills-int-client-js
Bumps [axios](https://github.com/axios/axios) from 1.12.0 to 1.13.5.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.12.0...v1.13.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-04-06 17:38:15 +00:00
rmmayo 266bf9911d updated included licenses 2026-03-26 11:54:49 +00:00
rmmayo 8ab6ab5a3a upgraded spring boot 2026-03-26 11:37:00 +00:00
rmmayo d747c2b96f upgraded spring boot 2026-03-24 12:07:28 +00:00
Dima May 002ab3f783 Merge pull request #267 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-js/axios-1.13.5
Bump axios from 1.12.0 to 1.13.5 in /skills-client-js
2026-03-17 13:44:58 -04:00
Dima May a5f3848c16 Merge pull request #268 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-js/rollup-4.59.0
Bump rollup from 4.45.1 to 4.59.0 in /skills-client-js
2026-03-17 13:43:31 -04:00
rmmayo 601c7a2a96 upgraded spring boot 2026-03-02 18:26:52 +00:00
dependabot[bot] 8fcc10d52f Bump rollup from 4.45.1 to 4.59.0 in /skills-client-js
Bumps [rollup](https://github.com/rollup/rollup) from 4.45.1 to 4.59.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.45.1...v4.59.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.59.0
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-28 09:07:40 +00:00
dependabot[bot] 65881829c5 Bump axios from 1.12.0 to 1.13.5 in /skills-client-js
Bumps [axios](https://github.com/axios/axios) from 1.12.0 to 1.13.5.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.12.0...v1.13.5)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.13.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2026-02-11 17:50:46 +00:00
rmmayo 99f946f33a upgraded spring boot to v3.5.10 2026-01-28 13:25:19 +00:00
Dima May 8051bf7a63 Merge pull request #266 from NationalSecurityAgency/t#3596/upgrade-spring-boot
Update spring boot
2025-11-25 14:14:36 -05:00
Don Walizer 9863d82e97 Update spring boot 2025-11-25 13:38:23 -05:00
Dima May a56fef71fb removed override for tomcat 2025-11-03 14:40:37 +00:00
Dima May db24520d75 upgraded spring and commons-lang 2025-10-24 10:06:13 +00:00
Dima May ca23b4e4d4 Merge pull request #264 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-integration/skills-int-client-js/axios-1.12.0
Bump axios from 1.10.0 to 1.12.0 in /skills-client-integration/skills-int-client-js
2025-09-23 20:11:29 +00:00
Dima May 9e9952eb53 Merge pull request #265 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-js/axios-1.12.0
Bump axios from 1.11.0 to 1.12.0 in /skills-client-js
2025-09-23 20:11:22 +00:00
dependabot[bot] 90d63e19dd Bump axios from 1.11.0 to 1.12.0 in /skills-client-js
Bumps [axios](https://github.com/axios/axios) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.11.0...v1.12.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-09-23 12:52:25 +00:00
rmmayo fe91a737d5 upgraded spring-boot to 3.5.6 2025-09-23 12:51:25 +00:00
dependabot[bot] 419f5c9968 Bump axios in /skills-client-integration/skills-int-client-js
Bumps [axios](https://github.com/axios/axios) from 1.10.0 to 1.12.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.10.0...v1.12.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.12.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-09-13 15:59:43 +00:00
Dima May 809e361760 Merge pull request #263 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-js/axios-1.11.0
Bump axios from 1.10.0 to 1.11.0 in /skills-client-js
2025-08-22 14:27:39 +00:00
rmmayo f5cc10c7df upgraded spring-boot to 3.5.5 2025-08-21 14:35:11 +00:00
rmmayo c225388951 #3457 - updated tomcat 2025-08-18 15:45:24 +00:00
rmmayo 6d50e32d27 upgraded spring to v3.5.4 2025-07-24 14:34:22 +00:00
dependabot[bot] 6711223ad1 Bump axios from 1.10.0 to 1.11.0 in /skills-client-js
Bumps [axios](https://github.com/axios/axios) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.11.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-07-23 16:54:11 +00:00
Dima May 4e04972a09 creating_release_version_3.6.2 2025-07-17 15:37:53 +00:00
Dima May a22b53a356 #261: removed commented out script 2025-07-17 13:59:40 +00:00
Ryan Mayo 5e5aefd745 Merge pull request #262 from NationalSecurityAgency/t#261/ci_failures
T#261/ci failures
2025-07-17 13:54:27 +00:00
Dima May 54eca19513 #261: updated year 2025-07-17 13:33:59 +00:00
Dima May 41bfbcc9d9 #261: upgraded spring boot and JS libs 2025-07-17 12:13:51 +00:00
Dima May be1e821973 #261: upgraded spring boot and JS libs 2025-07-17 11:28:09 +00:00
Dima May e38c70d39a #261: added running unit tests back 2025-07-17 11:04:21 +00:00
Dima May 796f97342b #261: added running unit tests back 2025-07-17 10:59:17 +00:00
Dima May 02fe5e1c77 #261: added a missing build steps 2025-07-16 19:06:15 +00:00
Dima May c9bfb0ca1f #261: upgraded libs; enabled videos 2025-07-16 18:12:22 +00:00
rmmayo ad321f0169 upgraded spring boot 2025-07-14 16:25:55 +00:00
rmmayo be81478d02 upgraded spring boot 2025-07-14 16:20:06 +00:00
Dima May 03372a9e92 fixing CI 2025-06-20 18:21:35 +00:00
Dima May 7ec6092d6f upgraded node and java 2025-06-20 15:07:27 +00:00
Dima May 354500f018 upgraded spring and tomcat 2025-06-20 11:02:49 +00:00
Dima May 7458284c30 upgraded spring 2025-05-22 19:34:00 +00:00
Dima May f8156f45ab upgraded spring 2025-04-25 17:45:45 +00:00
rmmayo ce13000dcb upgraded springboot to 3.4.4 2025-04-01 19:29:36 +00:00
rmmayo 1664fa7ac7 upgraded springboot to 3.4.3 2025-03-19 20:32:48 +00:00
Dima May 1806136418 Merge pull request #260 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-integration/skills-int-client-js/axios-1.8.2
Bump axios from 1.7.7 to 1.8.2 in /skills-client-integration/skills-int-client-js
2025-03-11 08:06:05 -04:00
dependabot[bot] 281140c19f Bump axios in /skills-client-integration/skills-int-client-js
Bumps [axios](https://github.com/axios/axios) from 1.7.7 to 1.8.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.7...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2025-03-11 10:53:17 +00:00
Ryan Mayo 0763dfbaa3 Merge pull request #259 from NationalSecurityAgency/t#258/upgrade_libs
258: upgraded libs
2025-03-11 06:52:23 -04:00
Dima May b4a59fdb7e 258: upgraded libs 2025-03-10 19:48:01 +00:00
rmmayo 7759001feb #3058 - upgrading spring boot 2024-12-31 20:42:01 +00:00
rmmayo 1b3ac0ff11 upgraded spring boot 2024-11-26 15:32:38 +00:00
Dima May d43f21a4cc creating_release_version_3.6.1 2024-11-20 17:00:39 +00:00
Don Walizer 2d946cb9d9 Merge pull request #257 from NationalSecurityAgency/update_readme
updated README and project home page link
2024-11-20 11:57:14 -05:00
Dima May 8da314393b updated README and project home page link 2024-11-20 16:49:05 +00:00
Dima May e9a11fbbca updated README and project home page link 2024-11-20 16:36:28 +00:00
Dima May 0af837321c creating_release_version_3.6.0 2024-11-20 15:20:15 +00:00
Dima May 0b92440cbc Merge pull request #256 from NationalSecurityAgency/t#255/upgrade_spring_boot
#255 - upgrading spring boot
2024-11-19 06:49:58 -05:00
rmmayo 24e6aca4f8 #255 - upgrading spring boot 2024-11-18 17:58:24 +00:00
Ryan Mayo 5cb0d9f455 Merge pull request #254 from NationalSecurityAgency/t#209/disable_skills_client
T#209/disable skills client
2024-10-09 13:15:44 +00:00
Dima May f6fddedeae #251: improved readme 2024-10-09 12:38:07 +00:00
Dima May 6851ef187b #251: only run disabled test for 3.6.0 and above 2024-10-09 12:02:27 +00:00
Dima May 133019e4e5 #251: to avoid a race condition fixed index.html page to add success handle after configure method completed 2024-10-09 10:50:08 +00:00
Dima May 59c4ce4c70 #209: fix unit tests 2024-10-08 21:11:26 +00:00
Dima May 727a2b6cf6 #209: added an option to disable skills-client 2024-10-08 20:55:37 +00:00
Ryan Mayo 1d36f49586 Merge pull request #253 from NationalSecurityAgency/t#213/iframe_aria_label
#213: added aria-label to iframe
2024-10-08 20:20:55 +00:00
Dima May 7480f6ab2a #213: added aria-label to iframe 2024-10-08 19:23:22 +00:00
Dima May 2207dcc248 Update README.md 2024-10-08 15:13:46 +00:00
Dima May 139db488cc Merge pull request #250 from NationalSecurityAgency/dependabot/maven/skills-client-integration/skills-int-e2e-test/commons-io-commons-io-2.14.0
Bump commons-io:commons-io from 2.8.0 to 2.14.0 in /skills-client-integration/skills-int-e2e-test
2024-10-08 14:57:05 +00:00
Ryan Mayo 059e208b53 Merge pull request #252 from NationalSecurityAgency/t#251/compat_ci
T#251/compat ci
2024-10-08 14:26:53 +00:00
Dima May 7d8e8f5b53 #251: do not run unsupported test for lib prior 3.2.0 2024-10-08 13:50:19 +00:00
Dima May 0a15a7651f #251: upgraded all js libs for skills-client-js; reverted removal of clientDisplayDefaultOptions.html 2024-10-08 13:36:24 +00:00
Dima May d03a5b3fec #251: upgraded all js libs for skills-int-client-js; fixed tests to run with 3.2.0 version of skills-client-js 2024-10-08 13:21:25 +00:00
Dima May a9d0a674f4 #251: cleaning up 2024-10-07 20:40:07 +00:00
Dima May 2152366635 #251: fixed tests against sills-service 2.12.6 2024-10-07 16:12:15 +00:00
Dima May ad31359fdd #251: filter skills-service tags from testing 2024-10-07 14:45:37 +00:00
Dima May 610e844f8e #251: filter skills-service tags from testing 2024-10-07 14:44:38 +00:00
Dima May bfd93347a4 #251: filter skills-service tags from testing 2024-10-07 14:04:04 +00:00
Dima May eb6395844b #251: filter skills-service tags from testing 2024-10-07 14:02:07 +00:00
Dima May 0286c9e705 #251: filter skills-service tags from testing 2024-10-07 13:59:28 +00:00
Dima May e7a6ba07ce #251: filter skills-service tags from testing 2024-10-07 13:54:10 +00:00
Dima May 3144e84bd3 #251: filter skills-service tags from testing 2024-10-07 13:52:37 +00:00
Dima May 5f9d1daf0f #251: filter skills-service tags from testing 2024-10-07 13:49:07 +00:00
Dima May 9d844f8e67 #251: run skills-service backward compat tests 2024-10-04 17:44:16 +00:00
Dima May 303748a980 #251: run skills-service backward compat tests 2024-10-04 17:38:15 +00:00
Dima May 89b55047e9 #251: run skills-service backward compat tests 2024-10-04 17:34:38 +00:00
Dima May 0bc3f485a9 #251: run skills-service backward compat tests 2024-10-04 17:24:21 +00:00
Dima May 514c9555e0 #251: run skills-service backward compat tests 2024-10-04 17:23:13 +00:00
Dima May 94ed933a4b #251: run skills-service backward compat tests 2024-10-04 17:21:53 +00:00
Dima May 2cd9ed554d #251: run skills-service backward compat tests 2024-10-04 17:17:35 +00:00
Dima May a881f59e9c #251: run skills-service backward compat tests 2024-10-04 17:14:36 +00:00
Dima May c700ecd5f7 #251: run skills-service backward compat tests 2024-10-04 17:12:08 +00:00
Dima May 7bd9939acd #251: run skills-service backward compat tests 2024-10-04 17:07:19 +00:00
Dima May 884463917b #251: run skills-service backward compat tests 2024-10-04 17:04:40 +00:00
Dima May 55cc382f4b #251: run skills-service backward compat tests 2024-10-04 17:01:22 +00:00
Dima May da3a3cbf7f #251: run skills-service backward compat tests 2024-10-04 16:59:42 +00:00
Dima May fb02712eaf #251: run skills-service backward compat tests 2024-10-04 16:56:36 +00:00
Dima May 5fd7b97802 #251: run skills-service backward compat tests 2024-10-04 16:55:52 +00:00
Dima May d529670ebf #251: run skills-service backward compat tests 2024-10-04 16:36:11 +00:00
Dima May 9cf2f879b2 #251: run skills-service backward compat tests 2024-10-04 16:27:27 +00:00
Dima May e372fe335f #251: run skills-service backward compat tests 2024-10-04 16:18:33 +00:00
Dima May bf0a5c2b40 #251: run skills-service backward compat tests 2024-10-04 16:12:51 +00:00
Dima May db3b51fbb0 #251: run skills-service backward compat tests 2024-10-04 15:38:05 +00:00
dependabot[bot] 7fca01690c Bump commons-io:commons-io
Bumps commons-io:commons-io from 2.8.0 to 2.14.0.

---
updated-dependencies:
- dependency-name: commons-io:commons-io
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <[email protected]>
2024-10-03 18:51:34 +00:00
Dima May 6ebb19bcd0 Merge pull request #248 from NationalSecurityAgency/dependabot/npm_and_yarn/skills-client-js/rollup-4.22.4
Bump rollup from 4.22.2 to 4.22.4 in /skills-client-js
2024-10-01 09:28:26 -04:00
dependabot[bot] 47eb39b7a5 Bump rollup from 4.22.2 to 4.22.4 in /skills-client-js
Bumps [rollup](https://github.com/rollup/rollup) from 4.22.2 to 4.22.4.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.22.2...v4.22.4)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <[email protected]>
2024-10-01 11:03:36 +00:00
Ryan Mayo 66b9070fb2 Merge pull request #247 from NationalSecurityAgency/t#242/ci_and_upgrades
T#242/ci and upgrades
2024-10-01 07:03:09 -04:00
Dima May ee2f3acb04 #242: fixing oauth CI 2024-10-01 10:46:55 +00:00
Dima May 5aeba4fbdf #242: tests improvements 2024-10-01 10:20:26 +00:00
rmmayo c9d5e52b26 #245 - upgraded spring boot to 3.3.4 2024-10-01 00:52:43 +00:00
Dima May 1cc3cf90fb #242: fixing tests for oauth 2024-10-01 00:33:46 +00:00
Dima May efbcbd4691 #242: CI updates and fixes 2024-09-30 20:35:54 +00:00
Dima May 4015922833 #242: disable oauth tests on check-in for now 2024-09-30 12:49:03 +00:00
Dima May b74e94e4f8 #242: fixing tests 2024-09-30 12:38:46 +00:00
Dima May 0097090d64 #242: fixing CI 2024-09-30 12:06:27 +00:00
Dima May c8ebf124b5 #242: debug ci 2024-09-30 11:04:54 +00:00
Dima May 905fab93bd #242: add more transcript export tests; refactored cypress tests into multiple suites 2024-09-27 14:23:11 +00:00
Dima May 868eaefbec #242: kickoff tests 2024-09-27 12:46:08 +00:00
Dima May a15853c374 #242: inserted header 2024-09-27 12:10:24 +00:00
Dima May 17d1a7af4c #242: additional cypress test for back and forward navigation, breadcrumb navigation and a transcript download test 2024-09-26 20:36:26 +00:00
Dima May f1caf21a48 #242: fixing tests 2024-09-26 18:05:17 +00:00
Dima May 15d9d98e98 #242: fixing tests 2024-09-24 15:05:25 +00:00
Dima May af16c671cc #242: copy skills-service.jar in the proper location 2024-09-23 20:56:01 +00:00
Dima May d0f8995829 #242: skip tests 2024-09-23 20:42:35 +00:00
Dima May cd3fd7ba4b #242: checking out specific branch and building skills-service 2024-09-23 20:36:03 +00:00
Dima May 3d274eb8a2 #242: started fixing cypress specs; changed build to use native node 2024-09-23 20:30:56 +00:00
Dima May dd582da97d #242: started fixing cypress specs; changed build to use native node 2024-09-23 20:30:01 +00:00
Dima May 11ace4d8b6 #242: started fixing cypress specs; changed build to use native node 2024-09-23 20:20:20 +00:00
Dima May dc21097da1 upgrade github actions 2024-09-20 15:52:31 +00:00
Dima May 8bdeda36d3 upgraded skills-client-js libs 2024-09-20 15:45:28 +00:00
Dima May 323f2455b2 Merge pull request #241 from NationalSecurityAgency/upgrade-spring-3-3
Upgrade Spring Boot to 3.3.3
2024-09-05 13:17:35 -04:00
97 changed files with 2684 additions and 1443 deletions

No files matched your search

@@ -2,9 +2,16 @@
name: Continuous Integration expired token tests
env:
NODE_VERSION: 26
JAVA_VERSION: 25
on:
schedule:
- cron: '0 8 * * *'
push:
paths-ignore:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
@@ -34,16 +41,16 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3
- uses: actions/checkout@v6
- uses: actions/setup-node@v3
- uses: actions/setup-node@v6
with:
node-version: '14'
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v3
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: '19' # The JDK version to make available on the path.
java-version: ${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
@@ -53,7 +60,7 @@ jobs:
node --version
- name: Cache local Maven repository
uses: actions/cache@v3
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -61,7 +68,7 @@ jobs:
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v3
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -70,11 +77,25 @@ jobs:
# - name: build skill-service
# run: ./.github/workflows/scripts/getSkillsService.sh
- name: download skill-service
# - name: download skill-service
# run: |
# mkdir skills-service
# cd skills-service
# curl -s https://api.github.com/repos/NationalSecurityAgency/skills-service/releases/latest | grep browser_download_url | cut -d '"' -f 4 | wget -qi -
# ls ./
# cd ..
- name: Checkout specific branch fo skills-service
uses: actions/checkout@v6
with:
repository: NationalSecurityAgency/skills-service
path: skills-service
- name: build skill-service
run: |
mkdir skills-service
cd skills-service
curl -s https://api.github.com/repos/NationalSecurityAgency/skills-service/releases/latest | grep browser_download_url | cut -d '"' -f 4 | wget -qi -
mvn --batch-mode clean install -DskipTests
cp ./service/target/skills-service-*.jar ./
ls ./
cd ..
@@ -120,7 +141,7 @@ jobs:
- name: upload result artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v7
with:
name: result artifacts
path: |
+35 -24
View File
@@ -2,21 +2,17 @@
name: Continuous Integration with OAuth
env:
NODE_VERSION: 26
JAVA_VERSION: 25
on:
schedule:
- cron: '0 7 * * *'
push:
paths-ignore:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
workflow_dispatch:
#on:
# push:
# paths-ignore:
# - 'README.md'
# - .github/workflows/skills-service-backward-compat.yml
# - .github/workflows/client-libs-backward-compat.yml
# pull_request:
# paths-ignore:
# - 'README.md'
# - .github/workflows/skills-service-backward-compat.yml
# - .github/workflows/client-libs-backward-compat.yml
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
@@ -46,15 +42,16 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- uses: actions/setup-node@v2-beta
- uses: actions/setup-node@v6
with:
node-version: '14'
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v1
- uses: actions/setup-java@v5
with:
java-version: '19' # The JDK version to make available on the path.
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
@@ -64,7 +61,7 @@ jobs:
node --version
- name: Cache local Maven repository
uses: actions/cache@v2
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -72,18 +69,32 @@ jobs:
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v2
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: download skill-service
# - name: download skill-service
# run: |
# mkdir skills-service
# cd skills-service
# curl -s https://api.github.com/repos/NationalSecurityAgency/skills-service/releases/latest | grep browser_download_url | cut -d '"' -f 4 | wget -qi -
# ls ./
# cd ..
- name: Checkout specific branch fo skills-service
uses: actions/checkout@v6
with:
repository: NationalSecurityAgency/skills-service
path: skills-service
- name: build skill-service
run: |
mkdir skills-service
cd skills-service
curl -s https://api.github.com/repos/NationalSecurityAgency/skills-service/releases/latest | grep browser_download_url | cut -d '"' -f 4 | wget -qi -
mvn --batch-mode clean install -DskipTests
cp ./service/target/skills-service-*.jar ./
ls ./
cd ..
@@ -153,7 +164,7 @@ jobs:
- name: upload result artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v7
with:
name: result artifacts
path: |
+21 -22
View File
@@ -2,17 +2,16 @@
name: Continuous Integration
env:
NODE_VERSION: 26
JAVA_VERSION: 25
on:
push:
paths-ignore:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
pull_request:
paths-ignore:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
@@ -43,15 +42,16 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- uses: actions/setup-node@v2-beta
- uses: actions/setup-node@v6
with:
node-version: '14'
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v1
- uses: actions/setup-java@v5
with:
java-version: '19' # The JDK version to make available on the path.
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
@@ -61,26 +61,25 @@ jobs:
node --version
- name: Cache local Maven repository
uses: actions/cache@v2
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: download skill-service
- name: Checkout specific branch fo skills-service
uses: actions/checkout@v6
with:
repository: NationalSecurityAgency/skills-service
path: skills-service
- name: build skill-service
run: |
mkdir skills-service
cd skills-service
curl -s https://api.github.com/repos/NationalSecurityAgency/skills-service/releases/latest | grep browser_download_url | cut -d '"' -f 4 | wget -qi -
mvn --batch-mode clean install -DskipTests
cp ./service/target/skills-service-*.jar ./
ls ./
cd ..
@@ -112,7 +111,7 @@ jobs:
- name: upload result artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v7
with:
name: result artifacts
path: |
@@ -1,114 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: Client Libs Backward Compatibility
#on:
# push:
# paths-ignore:
# - 'README.md'
# pull_request:
# paths-ignore:
# - 'README.md'
on:
schedule:
- cron: '0 5 * * *'
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
test-client-libs-backward-compat:
# The type of runner that the job will run on
runs-on: ubuntu-latest
services:
postgres:
# Docker Hub image
image: postgres
# Provide the password for postgres
env:
POSTGRES_PASSWORD: skillsPassword
POSTGRES_DB: skills
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
with:
node-version: '14'
- uses: actions/setup-java@v1
with:
java-version: '19' # The JDK version to make available on the path.
- name: Print Versions
run: |
mvn --version
java -version
npm -version
node --version
- name: Cache local Maven repository
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v2
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Download Releases to Test
run: |
mkdir -p skills-service-versions
cd skills-service-versions
curl -s https://api.github.com/repos/NationalSecurityAgency/skills-service/releases | grep browser_download_url | cut -d '"' -f 4 | wget -qi -
ls ./
cd ../
- name: setup npm links
run: ./.github/workflows/scripts/setupNpmLinks.sh
- name: build integration test apps
run: |
cd ./skills-client-integration
mvn --batch-mode clean install
cd ../
- name: run backwards compat tests
run: |
cd skills-client-integration/skills-int-e2e-test
mvn --batch-mode clean package
export NO_COLOR=1
export CYPRESS_RECORD_KEY=${{ secrets.CYPRESS_RECORD_KEY }}
java -cp target/skills-int-e2e-test-*.jar -Dloader.main=skills.TestClientLibsBackwardsCompat org.springframework.boot.loader.launch.PropertiesLauncher -record -tag "${{ github.workflow }}" --minVersion "1.9.0"
cd ../../
- name: upload result artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: result artifacts
path: |
./skills-client-integration/skills-int-e2e-test/target/logs/*
./skills-client-integration/skills-int-e2e-test/target/skills-client/skills-client-integration/skills-int-e2e-test/cypress/videos/*.mp4
./skills-client-integration/skills-int-e2e-test/target/skills-client/skills-client-integration/skills-int-e2e-test/cypress/screenshots/**/*.png
./skills-client-integration/skills-int-e2e-test/cypress/videos/*.mp4
./skills-client-integration/skills-int-e2e-test/cypress/screenshots/**/*.png
@@ -0,0 +1,197 @@
name: Client Libs Backward Compatibility
env:
NODE_VERSION: 26
JAVA_VERSION: 25
on:
push:
paths-ignore:
- 'README.md'
- .github/workflows/skills-service-backward-compat.yml
- .github/workflows/client-libs-backward-compat.yml
workflow_dispatch:
jobs:
get_skills_service_versions:
runs-on: ubuntu-latest
outputs:
services: ${{ steps.generate-matrix.outputs.services }}
versions: ${{ steps.generate-matrix.outputs.versions }}
steps:
- name: Generate Matrix
id: generate-matrix
run: |
TO_EXCLUDE='["3.1.0","3.0.3","3.0.2","3.0.1","3.0.0"]'
RESPONSE=$(curl -s -X GET 'https://api.github.com/repos/NationalSecurityAgency/skills-service/releases')
ALL_TAG_NAMES=$(echo "$RESPONSE" | jq -c '[.[] | .tag_name]')
echo "ALL_TAG_NAMES=$ALL_TAG_NAMES"
FILTERED_TAG_NAMES=$(jq --argjson to_exclude "$TO_EXCLUDE" '. - $to_exclude' <<< "$ALL_TAG_NAMES")
echo "FILTERED_TAG_NAMES=$FILTERED_TAG_NAMES"
TOP_TAGS=$(jq -c '.[0:5]' <<< "$FILTERED_TAG_NAMES")
echo "TOP_TAGS=$TOP_TAGS"
echo ::set-output name=versions::${TOP_TAGS}
build_skills_client_int:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
mvn --version
java -version
npm -version
node --version
- name: Cache local Maven repository
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: setup npm links
run: ./.github/workflows/scripts/setupNpmLinks.sh
- name: build integration test apps
env:
MAVEN_OPTS: -Xmx2048m
run: |
cd ./skills-client-integration
mvn --batch-mode clean install
cd ../
- name: upload service jar
uses: actions/upload-artifact@v7
with:
name: service jar
path: ./skills-client-integration/skills-int-service/target/skills-int-service-*.jar
run_backward_compatibility_tests:
runs-on: ubuntu-latest
services:
postgres:
# Docker Hub image
image: postgres
# Provide the password for postgres
env:
POSTGRES_PASSWORD: skillsPassword
POSTGRES_DB: skills
# Set health checks to wait until postgres has started
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
# Maps tcp port 5432 on service container to the host
- 5432:5432
needs:
- get_skills_service_versions
- build_skills_client_int
strategy:
fail-fast: false
matrix:
version: ${{ fromJSON(needs.get_skills_service_versions.outputs.versions) }}
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: ${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
mvn --version
java -version
npm -version
node --version
- name: Cache local Maven repository
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: download skill-service:${{ matrix.version }}
run: |
mkdir skills-service
cd skills-service
wget https://github.com/NationalSecurityAgency/skills-service/releases/download/${{ matrix.version }}/skills-service-${{ matrix.version }}.jar
ls ./
cd ..
- name: download integration test apps
uses: actions/download-artifact@v8
with:
name: service jar
path: ./skills-client-integration/skills-int-service/target/
- name: start services for cypress tests
run: |
cd skills-client-integration/skills-int-e2e-test
npm install
npm run cyServices:start:skills-service:ci
npm run cyServices:start:integration-apps
cd ../../
- name: Run Cypress tests
run: |
cd skills-client-integration/skills-int-e2e-test
npx cypress run --env skills-service.minVersion=${{ matrix.version }}
cd ../../
- name: upload result artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v7
with:
name: result artifacts for ${{ matrix.version }}
path: |
./skills-service/*.jar
./skills-client-integration/skills-int-service/target/*.jar
./skills-client-integration/skills-int-e2e-test/target/logs/*.out
./skills-client-integration/skills-int-e2e-test/cypress/videos/*.mp4
./skills-client-integration/skills-int-e2e-test/cypress/screenshots/**/*.png
./skills-client-integration/skills-int-e2e-test/logs
./skills-client-integration/skills-int-e2e-test/cypress/videos
./skills-client-integration/skills-int-e2e-test/cypress/screenshots
./skills-client-integration/skills-int-e2e-test/cypress/snapshots/**/**/__diff_output__/*.png
./skills-client-integration/skills-int-e2e-test/cypress/snapshots/**/__diff_output__/*.png
@@ -1,4 +1,4 @@
# Copyright 2020 SkillTree
# Copyright 2025 SkillTree
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+1 -1
View File
@@ -1,4 +1,4 @@
# Copyright 2020 SkillTree
# Copyright 2025 SkillTree
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
+28 -8
View File
@@ -1,4 +1,4 @@
# Copyright 2020 SkillTree
# Copyright 2025 SkillTree
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,11 +17,31 @@ set -e
set -o pipefail
echo "------- START: Setup npm links -------"
cd skills-client-integration/skills-int-e2e-test
mvn --batch-mode clean package -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
ls -la target/skills-int-e2e-test-*.jar
du -sh target/*
java --version
java -cp target/skills-int-e2e-test-*.jar -Dloader.main=skills.SetupNpmLinks org.springframework.boot.loader.launch.PropertiesLauncher
cd ../../
cd skills-client-js
echo "------- skills-client-js: npm link -------"
npm link
cd ../skills-client-integration/skills-int-client-js/
echo "------- skills-int-client-js: npm link @skilltree/skills-client-js -------"
npm link @skilltree/skills-client-js
cd ../../skills-client-js
echo "------- skills-client-js: npm install -------"
npm install
npm run build
echo "------- skills-int-client-js: npm install -------"
cd ../skills-client-integration/skills-int-client-js/
npm install
echo "------- validate npm links -------"
if ! npm ls --link --global | grep "skills-client-js"; then
echo "Error: skills-client-js is not properly linked"
exit 1
else
echo "Verified: skills-client-js is properly linked"
fi
echo "------- skills-int-client-js: npm run build -------"
npm run build
echo "------- DONE: Setup npm links -------"
@@ -2,25 +2,89 @@
name: Skills-service Backward Compatibility
#on:
# push:
# paths-ignore:
# - 'README.md'
# pull_request:
# paths-ignore:
# - 'README.md'
env:
NODE_VERSION: 26
JAVA_VERSION: 25
on:
schedule:
- cron: '0 6 * * *'
push:
paths-ignore:
- 'README.md'
pull_request:
paths-ignore:
- 'README.md'
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
test-skill-service-backward-compat:
# The type of runner that the job will run on
build-skills-service:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: |
11
${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
mvn --version
java -version
npm -version
node --version
- name: Cache local Maven repository
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Checkout specific branch fo skills-service
uses: actions/checkout@v6
with:
repository: NationalSecurityAgency/skills-service
path: skills-service
- name: build skill-service
run: |
cd skills-service
mvn --batch-mode clean install -DskipTests
cd ..
- name: upload service jar
uses: actions/upload-artifact@v7
with:
name: service jar
path: ./skills-service/service/target/skills-service-*.jar
test-skill-service-backward-compat:
runs-on: ubuntu-latest
needs: [build-skills-service]
strategy:
fail-fast: false
matrix:
skillsJsVersion: [ '3.5.0', '3.6.0', '3.6.1', '3.6.2', '3.6.3']
services:
postgres:
# Docker Hub image
@@ -42,18 +106,18 @@ jobs:
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/checkout@v6
- uses: actions/setup-node@v2-beta
- uses: actions/setup-node@v6
with:
node-version: '14'
node-version: ${{ env.NODE_VERSION }}
- uses: actions/setup-java@v3
- uses: actions/setup-java@v5
with:
distribution: 'temurin'
java-version: |
11
19
${{ env.JAVA_VERSION }}
- name: Print Versions
run: |
@@ -63,7 +127,7 @@ jobs:
node --version
- name: Cache local Maven repository
uses: actions/cache@v2
uses: actions/cache@v6
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
@@ -71,7 +135,7 @@ jobs:
${{ runner.os }}-maven-
- name: Cache npm
uses: actions/cache@v2
uses: actions/cache@v6
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
@@ -80,31 +144,99 @@ jobs:
# - name: build skill-service
# run: ./.github/workflows/scripts/getSkillsService.sh
- name: download skill-service
run: |
mkdir skills-service
cd skills-service
curl -s https://api.github.com/repos/NationalSecurityAgency/skills-service/releases/latest | grep browser_download_url | cut -d '"' -f 4 | wget -qi -
ls ./
cd ..
# - name: download skill-service
# run: |
# mkdir skills-service
# cd skills-service
# curl -s https://api.github.com/repos/NationalSecurityAgency/skills-service/releases/latest | grep browser_download_url | cut -d '"' -f 4 | wget -qi -
# ls ./
# cd ..
- name: run backwards compat tests
# - name: Checkout specific branch fo skills-service
# uses: actions/checkout@v6
# with:
# repository: NationalSecurityAgency/skills-service
# path: skills-service
#
# - name: build skill-service
# run: |
# cd skills-service
# mvn --batch-mode clean install -DskipTests
# cp ./service/target/skills-service-*.jar ./
# ls ./
# cd ..
- uses: actions/download-artifact@v8
with:
name: service jar
path: ./skills-service/
- name: build integration test apps
env:
MAVEN_OPTS: -Xmx2048m
run: |
cd ./skills-client-integration/skills-int-client-js
npm install @skilltree/skills-client-js@${{ matrix.skillsJsVersion }} --save-exact
cd ..
mvn --batch-mode clean install
cd ../
- name: start services for cypress tests
run: |
cd skills-client-integration/skills-int-e2e-test
mvn --batch-mode clean package
export NO_COLOR=1
java -jar target/skills-int-e2e-test-*.jar
npm install
ls ../
echo "------"
ls ../../
echo "------"
ls ../../skills-service/
echo "------"
npm run cyServices:start:skills-service:ci
npm run cyServices:start:integration-apps
cd ../../
- name: Run Cypress tests
run: |
cd skills-client-integration/skills-int-e2e-test
npm run cy:run
cd ../../
- name: upload result artifacts
if: ${{ always() }}
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v7
with:
name: result artifacts
name: result artifacts for [${{ matrix.skillsJsVersion }}] version
path: |
./skills-client-integration/skills-int-e2e-test/target/logs/*
./skills-client-integration/skills-int-e2e-test/target/skills-client/skills-client-integration/skills-int-e2e-test/cypress/videos/*.mp4
./skills-client-integration/skills-int-e2e-test/target/skills-client/skills-client-integration/skills-int-e2e-test/cypress/screenshots/**/*.png
./skills-service/*.jar
./skills-client-integration/skills-int-service/target/*.jar
./skills-client-integration/skills-int-e2e-test/target/logs/*.out
./skills-client-integration/skills-int-e2e-test/cypress/videos/*.mp4
./skills-client-integration/skills-int-e2e-test/cypress/screenshots/**/*.png
/home/runner/.npm/_logs/*
./skills-client-integration/skills-int-e2e-test/logs
./skills-client-integration/skills-int-e2e-test/cypress/videos
./skills-client-integration/skills-int-e2e-test/cypress/screenshots
./skills-client-integration/skills-int-e2e-test/cypress/snapshots/**/**/__diff_output__/*.png
./skills-client-integration/skills-int-e2e-test/cypress/snapshots/**/__diff_output__/*.png
# - name: run backwards compat tests
# run: |
# cd skills-client-integration/skills-int-e2e-test
# mvn --batch-mode clean package
# export NO_COLOR=1
# java -jar target/skills-int-e2e-test-*.jar
# cd ../../
#
# - name: upload result artifacts
# if: ${{ always() }}
# uses: actions/upload-artifact@v7
# with:
# name: result artifacts
# path: |
# ./skills-client-integration/skills-int-e2e-test/target/logs/*
# ./skills-client-integration/skills-int-e2e-test/target/skills-client/skills-client-integration/skills-int-e2e-test/cypress/videos/*.mp4
# ./skills-client-integration/skills-int-e2e-test/target/skills-client/skills-client-integration/skills-int-e2e-test/cypress/screenshots/**/*.png
# ./skills-client-integration/skills-int-e2e-test/cypress/videos/*.mp4
# ./skills-client-integration/skills-int-e2e-test/cypress/screenshots/**/*.png
# /home/runner/.npm/_logs/*
+3
View File
@@ -15,3 +15,6 @@
/skills-client-ng/package-lock.json
/skills-client-ng/dist/
/skills-client-ng/projects/skilltree/skills-client-ng/src/version.js
/skills-client-integration/skills-int-e2e-test/cypress/downloads/*
AGENTS.md
+11 -5
View File
@@ -1,10 +1,16 @@
# SkillTree Client Libraries
<p>
<img src="https://img.shields.io/npm/d18m/%40skilltree%2Fskills-client-js" alt="npm downloads">
<img src="https://img.shields.io/npm/dw/%40skilltree%2Fskills-client-js" alt="npm weekly downloads">
<img src="https://img.shields.io/npm/l/%40skilltree%2Fskills-client-js" alt="License">
<img src="https://img.shields.io/npm/v/%40skilltree%2Fskills-client-js" alt="Latest Version">
<img alt="npm bundle size" src="https://img.shields.io/bundlephobia/minzip/%40skilltree%2Fskills-client-js">
</p>
SkillTree is an innovative approach to implementing application training. SkillTree skills-client project is an assembly of
client libraries that facilitate rapid integration with the SkillTree service.
SkillTree is an innovative approach to implementing application training. SkillTree's ``skills-client-js`` project facilitates rapid integration with the SkillTree service.
To learn about the SkillTree platform please visit our [Official Documentation](https://code.nsa.gov/skills-docs/).
These pages provide in-depth guidance on the installation, usage and contribution.
To learn about the SkillTree platform please visit our [Official Documentation](https://skilltreeplatform.dev/).
These pages provide in-depth guidance on the installation, usage and contribution.
## Workflow Status
@@ -16,7 +22,7 @@ These pages provide in-depth guidance on the installation, usage and contributio
[![Skills-service Backward Compatibility](https://github.com/NationalSecurityAgency/skills-client/actions/workflows/skills-service-backward-compat.yml/badge.svg)](https://github.com/NationalSecurityAgency/skills-client/actions/workflows/skills-service-backward-compat.yml)
[![Client Libs Backward Compatibility](https://github.com/NationalSecurityAgency/skills-client/actions/workflows/client-libs-backward-compat.yml/badge.svg)](https://github.com/NationalSecurityAgency/skills-client/actions/workflows/client-libs-backward-compat.yml)
[![Client Libs Backward Compatibility](https://github.com/NationalSecurityAgency/skills-client/actions/workflows/client-libs-backwards-compat.yml/badge.svg)](https://github.com/NationalSecurityAgency/skills-client/actions/workflows/client-libs-backwards-compat.yml)
@@ -1,4 +1,4 @@
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+2 -5
View File
@@ -7,13 +7,13 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<version>4.1.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>skills</groupId>
<artifactId>skills-client-integration</artifactId>
<packaging>pom</packaging>
<version>3.5.0-SNAPSHOT</version>
<version>3.6.5-SNAPSHOT</version>
<licenses>
<license>
@@ -31,9 +31,6 @@
<properties>
<!--suppress UnresolvedMavenProperty -->
<nexusServer>${nexus.url}</nexusServer>
<frontend-maven-plugin.version>1.10.3</frontend-maven-plugin.version>
<node.version>v14.15.0</node.version>
<java.version>19</java.version>
</properties>
</project>
@@ -1,5 +1,5 @@
<!--
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,14 +17,14 @@ limitations under the License.
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Skills Examples - Native Javascript</title>
<title>Skill Client Test</title>
<link rel="icon" type="image/x-icon" href="./skilltree.ico">
<link rel="stylesheet" type="text/css" href="./assets/app.css"/>
<link rel="stylesheet" type="text/css" href="./assets/js/bootstrap/dist/css/bootstrap.css">
<script type="text/javascript" src="./assets/js/jquery/dist/jquery.js"></script>
<script type="text/javascript" src="./assets/js/axios/dist/axios.min.js"></script>
<script type="text/javascript" src="./assets/js/js-beautify/js/lib/beautify.js"></script>
<script type="text/javascript" src="./assets/js/bootstrap/dist/js/bootstrap.bundle.js"></script>
<script type="text/javascript" src="assets/js/@skilltree/skills-client-js/dist/skills-client-js.umd.min.js"></script>
@@ -32,15 +32,19 @@ limitations under the License.
<script type="text/javascript">
const params = new URLSearchParams(window.location.search);
const isSummaryOnly = params.get('isSummaryOnly') ? JSON.parse(params.get('isSummaryOnly')) : false;
const internalBackButton = params.get('internalBackButton') ? JSON.parse(params.get('internalBackButton')) : false;
let clientDisplay = null;
const options = {
autoScrollStrategy: 'top-of-page',
isSummaryOnly,
internalBackButton,
};
const internalBackButtonParam = 'internalBackButton'
if (params.get(internalBackButtonParam)) {
options.internalBackButton = JSON.parse(params.get(internalBackButtonParam))
}
const themes = {
default: {
name: 'Bright (default)',
@@ -155,32 +159,38 @@ limitations under the License.
<div id="app">
<div>
<div>
<nav class="navbar navbar-dark bg-info">
<a href="#" class="navbar-brand">Native Javascript Integration Examples</a>
<ul class="navbar-nav flex-row mr-auto">
<li class="nav-item pr-3">
<a class="nav-link" href="index.html" data-cy="reportSkillsLink">Report Skill Events</a>
</li>
<li class="nav-item active pr-3">
<a class="nav-link" href="javascript:void(0)" data-cy="userDisplayLink">User Display</a>
</li>
</ul>
</nav>
</div>
<nav class="navbar navbar-expand-lg navbar-light" style="background-color: #e3f2fd;">
<div class="container-fluid">
<a href="#" class="navbar-brand">Client Test</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" aria-current="page" href="index.html" data-cy="reportSkillsLink">Report Skill Events</a>
</li>
<li class="nav-item">
<a class="nav-link active" href="javascript:void(0)" data-cy="userDisplayLink">User Display</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid mt-3">
<div class="container">
<div class="">
<div class="form-row align-items-center">
<div class="d-inline dropdown">
<button class="btn btn-outline-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<button class="btn btn-primary dropdown-toggle" type="button" id="dropdownMenuButton" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Change Theme
</button>
<div class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<ul class="dropdown-menu" aria-labelledby="dropdownMenuButton">
<a class="dropdown-item" href="javascript:void(0)" id="default-theme-dropdown-item" onclick="applyTheme(themes.default)">Bright (Default)</a>
<a class="dropdown-item" href="javascript:void(0)" id="darkblue-theme-dropdown-item" onclick="applyTheme(themes.darkBlue)">Dark Blue</a>
</div>
</ul>
</div>
<div class="d-inline custom-control custom-checkbox mx-2">
@@ -192,26 +202,19 @@ limitations under the License.
<label class="custom-control-label" for="internalBackButtonControl">Internal Back Button</label>
</div>
<button class="btn btn-outline-primary" type="button" data-cy="navigateButton" onclick="navigate()">
<button class="btn btn-primary" type="button" data-cy="navigateButton" onclick="navigate()">
Navigate
</button>
</div>
<div>
<div class="my-3">
<span id="skillsDisplayPath" data-cy="skillsDisplayPath"></span>
</div>
<div
style="min-height: 860px">
<div id="skills-client-display" />
<div id="skills-client-display"></div>
</div>
</div>
</div>
<footer class="text-muted bg-light py-5">
<div class="container">
<p>Native Javascript Examples Guide</p>
<p></p>
</div>
</footer>
</div>
</div>
@@ -220,5 +223,5 @@ limitations under the License.
<script type="text/javascript">
document.getElementById("displayTypeControl").checked = isSummaryOnly;
document.getElementById("internalBackButtonControl").checked = internalBackButton;
document.getElementById("internalBackButtonControl").checked = params.get(internalBackButtonParam) ? JSON.parse(params.get(internalBackButtonParam)) : false
</script>
@@ -1,5 +1,5 @@
<!--
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,14 +17,14 @@ limitations under the License.
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Skills Examples - Native Javascript</title>
<title>Skill Client Test</title>
<link rel="icon" type="image/x-icon" href="./skilltree.ico">
<link rel="stylesheet" type="text/css" href="./assets/app.css"/>
<link rel="stylesheet" type="text/css" href="./assets/js/bootstrap/dist/css/bootstrap.css">
<script type="text/javascript" src="./assets/js/jquery/dist/jquery.js"></script>
<script type="text/javascript" src="./assets/js/axios/dist/axios.min.js"></script>
<script type="text/javascript" src="./assets/js/js-beautify/js/lib/beautify.js"></script>
<script type="text/javascript" src="./assets/js/bootstrap/dist/js/bootstrap.bundle.js"></script>
<script type="text/javascript" src="assets/js/@skilltree/skills-client-js/dist/skills-client-js.umd.min.js"></script>
@@ -53,37 +53,12 @@ limitations under the License.
<body>
<div id="app">
<div>
<div>
<nav class="navbar navbar-dark bg-info">
<a href="#" class="navbar-brand">Native Javascript Integration Examples</a>
<ul class="navbar-nav flex-row mr-auto">
<li class="nav-item pr-3">
<a class="nav-link" href="index.html" data-cy="reportSkillsLink">Report Skill Events</a>
</li>
<li class="nav-item active pr-3">
<a class="nav-link" href="javascript:void(0)" data-cy="userDisplayLink">User Display</a>
</li>
</ul>
</nav>
</div>
<div class="container-fluid mt-3">
<div class="container">
<div
style="min-height: 860px">
<div id="skills-client-display" />
</div>
<div class="container-fluid mt-3">
<div class="container">
<div style="min-height: 860px">
<div id="skills-client-display"></div>
</div>
</div>
<footer class="text-muted bg-light py-5">
<div class="container">
<p>Native Javascript Examples Guide</p>
<p></p>
</div>
</footer>
</div>
</div>
@@ -0,0 +1,73 @@
<!--
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Skill Client Test</title>
<link rel="icon" type="image/x-icon" href="./skilltree.ico">
<link rel="stylesheet" type="text/css" href="./assets/app.css"/>
<link rel="stylesheet" type="text/css" href="./assets/js/bootstrap/dist/css/bootstrap.css">
<script type="text/javascript" src="./assets/js/jquery/dist/jquery.js"></script>
<script type="text/javascript" src="./assets/js/axios/dist/axios.min.js"></script>
<script type="text/javascript" src="./assets/js/bootstrap/dist/js/bootstrap.bundle.js"></script>
<script type="module">
import { SkillsReporter, SkillsConfiguration, SkillsLevelJS, SkillsDisplayJS, Logger } from './assets/js/@skilltree/skills-client-js/dist/skills-client-js.esm.min.js'
axios.get("/api/config")
.then((result) => {
SkillsConfiguration.configure({...result.data, enabled: false});
});
document.querySelector('#reportSkillBtn').addEventListener('click', () => {
SkillsReporter.reportSkill('someId')
});
SkillsConfiguration.afterConfigure().then(() => {
const initializeSkillsDisplay = () => {
const clientDisplay = new SkillsDisplayJS();
clientDisplay.attachTo(document.querySelector('#skills-client-display'));
};
if (document.readyState === "complete"
|| document.readyState === "loaded"
|| document.readyState === "interactive") {
initializeSkillsDisplay();
} else {
document.addEventListener("DOMContentLoaded", initializeSkillsDisplay);
}
});
</script>
</head>
<body>
<div id="app">
<button
id="reportSkillBtn"
data-cy="reportSkillButton"
class="btn btn-outline-primary">
Report Skill
</button>
<div data-cy="levelComponentRes">Level: [<span id="skillLevel"/>]</div>
<div data-cy="skillsDisplayRes">Skills Display: [<div id="skills-client-display"></div>]</div>
</div>
</body>
</html>
@@ -1,5 +1,5 @@
<!--
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,13 +17,15 @@ limitations under the License.
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Skills Examples - Native Javascript</title>
<title>Skill Client Test</title>
<link rel="icon" type="image/x-icon" href="./skilltree.ico">
<link rel="stylesheet" type="text/css" href="./assets/app.css"/>
<link rel="stylesheet" type="text/css" href="./assets/js/bootstrap/dist/css/bootstrap.css">
<script type="text/javascript" src="./assets/js/jquery/dist/jquery.js"></script>
<script type="text/javascript" src="./assets/js/axios/dist/axios.min.js"></script>
<script type="text/javascript" src="./assets/js/js-beautify/js/lib/beautify.js"></script>
<script type="text/javascript" src="./assets/js/bootstrap/dist/js/bootstrap.bundle.js"></script>
<script type="module">
import { SkillsReporter, SkillsConfiguration, SkillsLevelJS, Logger } from './assets/js/@skilltree/skills-client-js/dist/skills-client-js.esm.min.js'
@@ -32,43 +34,43 @@ limitations under the License.
.then((result) => {
SkillsConfiguration.configure(result.data);
});
SkillsReporter.addSuccessHandler((result) => {
document.querySelector('#global-event-result-container-js').innerHTML = JSON.stringify(result, null, 2);
});
SkillsReporter.addErrorHandler((result) => {
document.querySelector('#global-event-result-container-js').innerHTML = JSON.stringify(result, null, 2);
});
const reportSkill = (skillId) => {
SkillsReporter.reportSkill(skillId)
.then((response) => {
document.querySelector('#report-result-container').innerHTML = JSON.stringify(response, null, 2);
}).catch((err) => {
document.querySelector('#report-result-container').innerHTML = JSON.stringify(err, null, 2);
});
};
document.querySelector('#exampleDirectiveClickEvent').addEventListener('click', () => {
var skillId = document.getElementById('skillId').value;
reportSkill(skillId);
});
const toggleAllEvents = () => {
const notifyIfSkillNotApplied = document.querySelector('#all-events-check').checked
SkillsReporter.configure( { notifyIfSkillNotApplied });
}
document.querySelector('#all-events-check').addEventListener('click', toggleAllEvents);
toggleAllEvents();
SkillsConfiguration.afterConfigure()
.then(() => {
if (window.Cypress) {
window.skillsLogger = Logger;
}
const skillLevel = new SkillsLevelJS();
skillLevel.attachTo('#skillLevel')
});
.then(() => {
SkillsReporter.addSuccessHandler((result) => {
document.querySelector('#global-event-result-container-js').innerHTML = JSON.stringify(result, null, 2);
});
SkillsReporter.addErrorHandler((result) => {
document.querySelector('#global-event-result-container-js').innerHTML = JSON.stringify(result, null, 2);
});
const reportSkill = (skillId) => {
SkillsReporter.reportSkill(skillId)
.then((response) => {
document.querySelector('#report-result-container').innerHTML = JSON.stringify(response, null, 2);
}).catch((err) => {
document.querySelector('#report-result-container').innerHTML = JSON.stringify(err, null, 2);
});
};
document.querySelector('#exampleDirectiveClickEvent').addEventListener('click', () => {
var skillId = document.getElementById('skillId').value;
reportSkill(skillId);
});
const toggleAllEvents = () => {
const notifyIfSkillNotApplied = document.querySelector('#all-events-check').checked
SkillsReporter.configure({notifyIfSkillNotApplied});
}
document.querySelector('#all-events-check').addEventListener('click', toggleAllEvents);
toggleAllEvents();
if (window.Cypress) {
window.skillsLogger = Logger;
}
const skillLevel = new SkillsLevelJS();
skillLevel.attachTo('#skillLevel')
});
</script>
</head>
@@ -77,20 +79,26 @@ limitations under the License.
<div id="app">
<div>
<div>
<nav class="navbar navbar-dark bg-info">
<a href="#" class="navbar-brand">Native Javascript Integration Examples</a>
<ul class="navbar-nav flex-row mr-auto">
<li class="nav-item active pr-3">
<a class="nav-link" href="javascript:void(0)" data-cy="reportSkillsLink">Report Skill Events</a>
</li>
<li class="nav-item pr-3">
<a class="nav-link" href="clientDisplay.html" data-cy="userDisplayLink">User Display</a>
</li>
</ul>
<button type="button" class="btn btn-primary">
<span id="skillLevel"/>
</button>
<nav class="navbar navbar-expand-lg navbar-light" style="background-color: #e3f2fd;">
<div class="container-fluid">
<a href="#" class="navbar-brand">Client Test</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="javascript:void(0)" data-cy="reportSkillsLink">Report Skill Events</a>
</li>
<li class="nav-item">
<a class="nav-link" href="clientDisplay.html" data-cy="userDisplayLink">User Display</a>
</li>
</ul>
<button type="button" class="btn btn-primary">
<span id="skillLevel"/>
</button>
</div>
</div>
</nav>
</div>
@@ -140,13 +148,6 @@ limitations under the License.
</div>
</div>
<footer class="text-muted bg-light py-5">
<div class="container">
<p>Native Javascript Examples Guide</p>
<p></p>
</div>
</footer>
</div>
</div>
Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,6 +1,6 @@
{
"name": "skills-int-client-js",
"version": "3.5.0",
"version": "3.6.5",
"license": "Apache-2.0",
"description": "SkillTree Client Lib Integration Tests - Native JS",
"main": "index.js",
@@ -12,20 +12,19 @@
"licenseHeaderCheck": "license-check-and-add check -f '../license-add/license-add-config.json'",
"licenseHeaderAdd": "license-check-and-add add -f ../license-add/license-add-config.json",
"licenseCheck": "npm run licenseHeaderCheck && npm run licenseDepCheck",
"build": "npm run licenseCheck && echo nothingelsetodo"
"build": "npm install && npm run licenseCheck && echo nothingelsetodo"
},
"author": "SkillTree Team",
"dependencies": {
"@skilltree/skills-client-js": "3.5.0",
"axios": "1.7.4",
"bootstrap": "4.5.2",
"jquery": "3.5.1",
"js-beautify": "1.13.0"
"@skilltree/skills-client-js": "3.6.5",
"axios": "1.20.0",
"bootstrap": "5.3.8",
"jquery": "4.0.0"
},
"devDependencies": {
"fs-extra": "8.1.0",
"license-check-and-add": "3.0.4",
"fs-extra": "11.4.0",
"license-check-and-add": "4.0.5",
"license-checker": "25.0.1",
"live-server": "1.2.1"
"live-server": "1.2.2"
}
}
}
@@ -7,7 +7,7 @@
<parent>
<groupId>skills</groupId>
<artifactId>skills-client-integration</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>3.6.5-SNAPSHOT</version>
</parent>
<artifactId>skills-int-client-js</artifactId>
@@ -19,55 +19,26 @@
<build>
<plugins>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>${frontend-maven-plugin.version}</version>
<configuration>
<installDirectory>target</installDirectory>
</configuration>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.5.1</version>
<executions>
<execution>
<id>install node and npm</id>
<id>exec-npm-install</id>
<phase>install</phase>
<goals>
<goal>install-node-and-npm</goal>
<goal>exec</goal>
</goals>
<configuration>
<nodeVersion>${node.version}</nodeVersion>
<executable>npm</executable>
<arguments>
<argument>run</argument>
<argument>build</argument>
</arguments>
<workingDirectory>${basedir}</workingDirectory>
</configuration>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<phase>generate-resources</phase>
<configuration>
<arguments>--unsafe-perm install</arguments>
</configuration>
</execution>
<execution>
<id>npm run build</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
<!--TODO: uncomment when we have at least 1 unit test-->
<!--<execution>-->
<!--<id>npm run test</id>-->
<!--<goals>-->
<!--<goal>npm</goal>-->
<!--</goals>-->
<!--<phase>test</phase>-->
<!--<configuration>-->
<!--<arguments>run unit</arguments>-->
<!--</configuration>-->
<!--</execution>-->
</executions>
</plugin>
</plugins>
</build>
@@ -4,6 +4,7 @@ module.exports = defineConfig({
chromeWebSecurity: false,
defaultCommandTimeout: 10000,
projectId: '3yas4t',
video: true,
retries: {
runMode: 2,
openMode: 0,
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,10 @@ export default {
const versionToCompare = Cypress.env(lib);
return this.laterThanMinVersion(minVersion, versionToCompare);
},
skillsClientJSVersionLaterThan(minVersion) {
const versionToCompare = Cypress.env('skills-client-js-version')
return this.laterThanMinVersion(minVersion, versionToCompare);
},
skillsServiceVersionLaterThan(minVersion) {
const versionToCompare = Cypress.env('skills-service.minVersion');
return this.laterThanMinVersion(minVersion, versionToCompare);
@@ -30,5 +34,20 @@ export default {
}
const shouldSkip = semver.lt(currentVersion, minVersion);
return !shouldSkip;
}
},
laterThan_3_1_0() {
return this.skillsServiceVersionLaterThan('3.1.0');
},
laterThan_1_4_0() {
return this.skillsServiceVersionLaterThan('1.4.0');
},
laterThan_1_11_1() {
return this.skillsServiceVersionLaterThan('1.11.1');
},
noThemeBackground() {
return 'rgb(255, 255, 255)';
},
rankDetailsTitle() {
return this.laterThan_1_11_1() ? 'My Rank' : 'Rank Overview'
},
}
@@ -0,0 +1,49 @@
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Utils from "./Utils";
import selectors from "../support/selectors";
context("Configuration Disabled Tests", () => {
it('export user transcript', () => {
cy.onlyOn(Utils.skillsClientJSVersionLaterThan('3.6.0'))
cy.createDefaultTinyProject('proj1')
// visit client display
cy.intercept('**/public/status', cy.spy().as('getStatus'))
cy.intercept('POST', '**/api/projects/proj1/skillsClientVersion', cy.spy().as('reportClientVersion'))
cy.intercept('**/api/projects/proj1/level', cy.spy().as('getLevel'))
cy.intercept('**/skills-websocket/info**', cy.spy().as('initWebsockets'))
cy.intercept('POST', '** /api/projects/proj1/skills/someId', cy.spy().as('reportSkillId'))
cy.intercept('/api/projects/proj1/summary', cy.spy().as('getProjSummary'))
cy.visit('/native/disabledConfig.html');
cy.wait(5000)
cy.get('@getStatus').should('not.have.been.called');
cy.get('@reportClientVersion').should('not.have.been.called');
cy.get('@getLevel').should('not.have.been.called');
cy.get('@initWebsockets').should('not.have.been.called');
cy.get('[data-cy="levelComponentRes"]').contains('Level: []')
cy.get('[data-cy="skillsDisplayRes"] #skills-client-display').should('be.empty')
cy.get('@getProjSummary').should('not.have.been.called');
cy.get('[data-cy="reportSkillButton"]').click()
cy.wait(3000)
cy.get('@reportSkillId').should('not.have.been.called');
});
});
@@ -0,0 +1,107 @@
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Utils from "./Utils";
import selectors from "../support/selectors";
const homePage = '/native/index.html'
context("Global Events Tests", () => {
const level1CompleteRegex = /"completed"\s*:\s*\[(?=[\s\S]*?\{[^{}]*?(?:"type"\s*:\s*"Overall"[^{}]*?"level"\s*:\s*1|"level"\s*:\s*1[^{}]*?"type"\s*:\s*"Overall")[^{}]*?\})[\s\S]*?\]/;
it("global event show correct results", () => {
cy.createDefaultProject();
const sendEventViaDropdownId = "#exampleDirectiveClickEvent";
Cypress.Commands.add("clickSubmit", () => {
cy.get(`${sendEventViaDropdownId} .btn`).click();
});
cy.visitHomePage(homePage);
cy.clickSubmit();
cy.get('pre[data-cy=globalEventResult]').contains('"skillId": "IronMan"')
cy.get('pre[data-cy=globalEventResult]').contains('"pointsEarned": 50')
cy.get('pre[data-cy=globalEventResult]').contains('"skillApplied": true')
cy.get('pre[data-cy=globalEventResult]').contains(level1CompleteRegex)
});
it("global event show correct results (skills reported directly to backend endpoint)", () => {
cy.createDefaultProject();
cy.visitHomePage(homePage);
cy.reportSkillForUser("IronMan", Cypress.env('proxyUser'));
cy.get('pre[data-cy=globalEventResult]').should('be.visible')
cy.get('pre[data-cy=globalEventResult]').contains('"skillId": "IronMan"')
cy.get('pre[data-cy=globalEventResult]').contains('"pointsEarned": 50')
cy.get('pre[data-cy=globalEventResult]').contains('"skillApplied": true')
cy.get('pre[data-cy=globalEventResult]').contains(level1CompleteRegex)
});
it('global event does not update when skill reported for a different project', () => {
cy.createDefaultProject()
cy.createDefaultTinyProject('proj2')
cy.visitHomePage(homePage);
cy.reportSkillForUser('IronMan', Cypress.env('proxyUser'), 'proj2')
cy.get('pre[data-cy=globalEventResult]').should('be.empty');
})
it('global event is not reported when skill is not applied (skill reported directly to backend endpoint)', () => {
cy.createDefaultProject()
cy.reportSkillForUser('IronMan', Cypress.env('proxyUser'))
cy.visitHomePage(homePage);
cy.reportSkillForUser('IronMan', Cypress.env('proxyUser'))
cy.get('[data-cy=globalEventResult]').should('be.empty');
})
it('global event is not reported when skill is not applied', () => {
cy.createDefaultProject()
cy.reportSkillForUser('IronMan', Cypress.env('proxyUser'))
cy.visitHomePage(homePage);
const sendEventViaDropdownId = "#exampleDirectiveClickEvent";
Cypress.Commands.add("clickSubmit", () => {
cy.get(`${sendEventViaDropdownId} .btn`).click();
});
cy.get('[type="checkbox"]').uncheck()
cy.clickSubmit()
cy.get('pre[data-cy=globalEventResult]').should('be.empty');
})
it('global event is reported even when skill is not applied when notifyIfSkillNotApplied=true ', () => {
cy.createDefaultProject()
cy.reportSkillForUser('IronMan', Cypress.env('proxyUser'))
const sendEventViaDropdownId = "#exampleDirectiveClickEvent";
Cypress.Commands.add("clickSubmit", () => {
cy.get(`${sendEventViaDropdownId} .btn`).click();
});
cy.visitHomePage(homePage);
cy.get('[type="checkbox"]').check()
cy.clickSubmit()
cy.get('pre[data-cy=globalEventResult]').contains('"skillId": "IronMan"')
cy.get('pre[data-cy=globalEventResult]').contains('"pointsEarned": 0')
cy.get('pre[data-cy=globalEventResult]').contains('"skillApplied": false')
cy.get('pre[data-cy=globalEventResult]').contains('"explanation": "This skill reached its maximum points"')
})
});
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,96 +14,11 @@
* limitations under the License.
*/
import Utils from "./Utils";
import selectors from "../support/selectors";
const homePage = '/native/index.html'
context("Native JS Tests", () => {
const laterThan_1_4_0 = Utils.skillsServiceVersionLaterThan('1.4.0');
const laterThan_1_11_1 = Utils.skillsServiceVersionLaterThan('1.11.1');
const noThemeBackground = laterThan_1_4_0 ? 'rgba(0, 0, 0, 0)' : 'rgb(255, 255, 255)';
const rankDetailsTitle = laterThan_1_11_1 ? 'My Rank' : 'Rank Overview'
it("global event show correct results", () => {
cy.createDefaultProject();
const sendEventViaDropdownId = "#exampleDirectiveClickEvent";
Cypress.Commands.add("clickSubmit", () => {
cy.get(`${sendEventViaDropdownId} .btn`).click();
});
cy.visitHomePage(homePage);
cy.clickSubmit();
cy.get('pre[data-cy=globalEventResult]').contains('"skillId": "IronMan"')
cy.get('pre[data-cy=globalEventResult]').contains('"pointsEarned": 50')
cy.get('pre[data-cy=globalEventResult]').contains('"skillApplied": true')
cy.get('pre[data-cy=globalEventResult]').contains(/completed": [[][^]*"type": "Overall",[^]\s*"level": 1/)
});
it("global event show correct results (skills reported directly to backend endpoint)", () => {
cy.createDefaultProject();
cy.visitHomePage(homePage);
cy.reportSkillForUser("IronMan", Cypress.env('proxyUser'));
cy.get('pre[data-cy=globalEventResult]').should('be.visible')
cy.get('pre[data-cy=globalEventResult]').contains('"skillId": "IronMan"')
cy.get('pre[data-cy=globalEventResult]').contains('"pointsEarned": 50')
cy.get('pre[data-cy=globalEventResult]').contains('"skillApplied": true')
cy.get('pre[data-cy=globalEventResult]').contains(/completed": [[][^]*"type": "Overall",[^]\s*"level": 1/)
});
it('global event does not update when skill reported for a different project', () => {
cy.createDefaultProject()
cy.createDefaultTinyProject('proj2')
cy.visitHomePage(homePage);
cy.reportSkillForUser('IronMan', Cypress.env('proxyUser'), 'proj2')
cy.get('pre[data-cy=globalEventResult]').should('be.empty');
})
it('global event is not reported when skill is not applied (skill reported directly to backend endpoint)', () => {
cy.createDefaultProject()
cy.reportSkillForUser('IronMan', Cypress.env('proxyUser'))
cy.visitHomePage(homePage);
cy.reportSkillForUser('IronMan', Cypress.env('proxyUser'))
cy.get('[data-cy=globalEventResult]').should('be.empty');
})
it('global event is not reported when skill is not applied', () => {
cy.createDefaultProject()
cy.reportSkillForUser('IronMan', Cypress.env('proxyUser'))
cy.visitHomePage(homePage);
const sendEventViaDropdownId = "#exampleDirectiveClickEvent";
Cypress.Commands.add("clickSubmit", () => {
cy.get(`${sendEventViaDropdownId} .btn`).click();
});
cy.get('[type="checkbox"]').uncheck()
cy.clickSubmit()
cy.get('pre[data-cy=globalEventResult]').should('be.empty');
})
it('global event is reported even when skill is not applied when notifyIfSkillNotApplied=true ', () => {
cy.createDefaultProject()
cy.reportSkillForUser('IronMan', Cypress.env('proxyUser'))
const sendEventViaDropdownId = "#exampleDirectiveClickEvent";
Cypress.Commands.add("clickSubmit", () => {
cy.get(`${sendEventViaDropdownId} .btn`).click();
});
cy.visitHomePage(homePage);
cy.get('[type="checkbox"]').check()
cy.clickSubmit()
cy.get('pre[data-cy=globalEventResult]').contains('"skillId": "IronMan"')
cy.get('pre[data-cy=globalEventResult]').contains('"pointsEarned": 0')
cy.get('pre[data-cy=globalEventResult]').contains('"skillApplied": false')
cy.get('pre[data-cy=globalEventResult]').contains('"explanation": "This skill reached its maximum points"')
})
context("Skills Display Tests", () => {
it("skill display", () => {
cy.createDefaultTinyProject();
@@ -113,7 +28,7 @@ context("Native JS Tests", () => {
userId: Cypress.env('proxyUser'),
timestamp: Date.now()
});
cy.visit("/native/clientDisplay.html");
cy.visitSkillsDisplay();
cy.wrapIframe().contains('My Level')
cy.wrapIframe().contains('50 Points earned Today')
@@ -121,8 +36,8 @@ context("Native JS Tests", () => {
// verify that there is no background set
// cypress always validates against rgb
cy.wrapIframe().find('.skills-page-title-text-color')
.should('have.css', 'background-color').and('equal', noThemeBackground);
cy.wrapIframe().find('[data-cy="skillsTitle"]')
.should('have.css', 'background-color').and('equal', Utils.noThemeBackground());
});
it("skill display - default options", () => {
@@ -139,11 +54,12 @@ context("Native JS Tests", () => {
cy.wrapIframe().contains('My Level')
cy.wrapIframe().contains('50 Points earned Today')
cy.wrapIframe().contains('Subject 0')
cy.wrapIframe().find(selectors.firstSubjectTileBtn)
// verify that there is no background set
// cypress always validates against rgb
cy.wrapIframe().find('.skills-page-title-text-color')
.should('have.css', 'background-color').and('equal', noThemeBackground);
cy.wrapIframe().find('[data-cy="skillsTitle"]')
.should('have.css', 'background-color').and('equal', Utils.noThemeBackground());
});
it("skill display - summary only", () => {
@@ -154,17 +70,17 @@ context("Native JS Tests", () => {
userId: Cypress.env('proxyUser'),
timestamp: Date.now()
});
cy.visit("/native/clientDisplay.html?isSummaryOnly=true");
cy.visitSkillsDisplay("?isSummaryOnly=true");
cy.wrapIframe().contains('My Level')
cy.wrapIframe().contains('50 Points earned Today')
cy.wrapIframe().contains('Subject 0').should('not.exist')
cy.wrapIframe().find('[data-cy="subjectTile-subj0"] [data-cy="subjectTileBtn"]').should('not.exist')
// verify that there is no background set
// cypress always validates against rgb
cy.wrapIframe().find('.skills-page-title-text-color')
.should('have.css', 'background-color').and('equal', noThemeBackground);
cy.wrapIframe().find('[data-cy="skillsTitle"]')
.should('have.css', 'background-color').and('equal', Utils.noThemeBackground());
});
it("skill display - theme", () => {
@@ -175,7 +91,7 @@ context("Native JS Tests", () => {
userId: Cypress.env('proxyUser'),
timestamp: Date.now()
});
cy.visit("/native/clientDisplay.html?themeName=Dark Blue");
cy.visitSkillsDisplay("?themeName=Dark Blue");
cy.wait("@getToken");
cy.wrapIframe().contains('My Level')
@@ -184,7 +100,7 @@ context("Native JS Tests", () => {
// verify dark blue background of hex #152E4d
// cypress always validates against rgb
cy.wrapIframe().find('.skills-page-title-text-color')
cy.wrapIframe().find('[data-cy="skillsTitle"]')
.should('have.css', 'background-color').and('equal', 'rgb(21, 46, 77)');
});
@@ -202,50 +118,54 @@ context("Native JS Tests", () => {
cy.wrapIframe().contains('My Level')
cy.wrapIframe().contains('50 Points earned Today')
cy.wrapIframe().contains('Subject 0').should('not.exist')
cy.wrapIframe().find('[data-cy="subjectTile-subj0"] [data-cy="subjectTileBtn"]').should('not.exist')
// verify dark blue background of hex #152E4d
// cypress always validates against rgb
cy.wrapIframe().find('.skills-page-title-text-color')
cy.wrapIframe().find('[data-cy="skillsTitle"]')
.should('have.css', 'background-color').and('equal', 'rgb(21, 46, 77)');
});
it('client display should display an error if skills service is down', () => {
cy.onlyOn(Utils.skillsClientJSVersionLaterThan('3.2.0'))
cy.createDefaultTinyProject()
cy.intercept('/public/status',
{
statusCode: 503, // server is down
body: {}
}).as('getStatus')
cy.visit('/native/clientDisplay.html');
cy.visitSkillsDisplay();
cy.wait('@getStatus')
cy.contains("Could NOT reach Skilltree Service");
});
it("only display skills up-to the provided version", () => {
const noVersionPoints = 'Earn up to 200 points';
const v1Points = 'Earn up to 150 points';
const v0Points = 'Earn up to 100 points';
const totalPointsSelector = '[data-cy="totalPoints"]'
cy.createDefaultTinyProject();
cy.intercept(Cypress.env('tokenUrl'))
.as("getToken");
cy.backendAddSkill("skillv1", 1);
cy.backendAddSkill("skillv2", 2);
cy.visit("/native/clientDisplay.html");
cy.visitSkillsDisplay();
cy.wait("@getToken");
cy.wait('@getToken')
cy.wrapIframe().contains(noVersionPoints);
const validateTotalPoints = (expectedPoints) => {
if (Utils.skillsServiceVersionLaterThan('3.0.0')){
cy.wrapIframe().find(totalPointsSelector).should('have.text', `${expectedPoints}`);
} else {
cy.wrapIframe().find('.user-skills-overview').contains(`Earn up to ${expectedPoints} points`)
}
}
validateTotalPoints(200)
// cy.visit("/native/index.html");
cy.visit("/native/clientDisplay.html?skillsVersion=1");
cy.visitSkillsDisplay("?skillsVersion=1");
cy.wait('@getToken')
cy.wrapIframe().contains(v1Points);
validateTotalPoints(150)
// cy.visit("/native/index.html");
cy.visit("/native/clientDisplay.html?skillsVersion=0");
cy.visitSkillsDisplay("?skillsVersion=0");
cy.wait('@getToken')
cy.wrapIframe().contains(v0Points);
validateTotalPoints(100)
});
it('skillsClientVersion is reported correctly', () => {
@@ -290,171 +210,4 @@ context("Native JS Tests", () => {
cy.contains('Level 1')
})
if (Utils.skillsServiceVersionLaterThan('1.5.0')) {
it('internal back button when when returning from an external page', () => {
cy.createDefaultTinyProject()
cy.intercept(Cypress.env('tokenUrl')).as('getToken')
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
cy.visit('/native/clientDisplay.html?internalBackButton=true');
// cy.wait('@getToken')
cy.clientDisplay(true).find('[data-cy=back]').should('not.exist');
cy.clientDisplay().contains('User Skills');
// navigate to Rank Overview that contains the back button
cy.clientDisplay().find('[data-cy=myRank]').click();
cy.clientDisplay().contains(rankDetailsTitle);
cy.clientDisplay().find('[data-cy=back]').should('exist');
// now visit the "Report Skills" (external) page
cy.get('[data-cy=reportSkillsLink]').click()
cy.contains('Report Skills Examples');
// switch back to the the client display
cy.get('[data-cy=userDisplayLink]').click()
});
it('internal back button when when returning from an external page - multiple layers deep', () => {
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
cy.visit('/native/clientDisplay.html?internalBackButton=true');
cy.clientDisplay().find('[data-cy=back]').should('not.exist');
cy.clientDisplay().contains('User Skills');
// to subject page
cy.cdClickSubj(0, 'Subject 0');
// navigate to Rank Overview that contains the back button
cy.clientDisplay().find('[data-cy=myRank]').click();
cy.clientDisplay().contains(rankDetailsTitle);
cy.clientDisplay().find('[data-cy=back]').should('exist');
// click the back button and verify that we are still in the
// client display (Subject page)
cy.cdBack('Subject 0');
// then back one more time and we should be back on the client display home page
cy.cdBack('User Skills');
});
it('browser back button works correctly when internal back button is not present', () => {
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
cy.visit("/native/clientDisplay.html?internalBackButton=false");
cy.clientDisplay().find('[data-cy=back]').should('not.exist');
cy.clientDisplay().contains('User Skills');
// to subject page
cy.cdClickSubj(0, 'Subject 0');
// navigate to Rank Overview and that it does NOT contains the internal back button
cy.clientDisplay().find('[data-cy=myRank]').click();
cy.clientDisplay().contains(rankDetailsTitle);
cy.clientDisplay().find('[data-cy=back]').should('not.exist');
// click the browser back button and verify that we are still in the
// client display (Subject page)
cy.wait(1000);
cy.go('back') // browser back button
cy.wait(1000);
cy.clientDisplay().contains('Subject 0');
// then back one more time and we should be back on the client display home page
cy.wait(1000);
cy.go('back') // browser back button
cy.wait(1000);
cy.clientDisplay().contains('User Skills');
});
it('deep link and reload', () => {
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// navigate to Rank Overview via direct link
cy.visit('/native/clientDisplay.html?skillsClientDisplayPath=%2Frank#/showSkills');
cy.clientDisplay().contains(rankDetailsTitle);
// reload and confirm we are still on Rank Overview page
cy.reload();
cy.clientDisplay().contains(rankDetailsTitle);
});
it('back button after reload', () => {
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
cy.visit("/native/clientDisplay.html?internalBackButton=false");
cy.clientDisplay().find('[data-cy=back]').should('not.exist');
cy.clientDisplay().contains('User Skills');
// to subject page
cy.cdClickSubj(0, 'Subject 0');
// navigate to Rank Overview and that it does NOT contains the internal back button
cy.clientDisplay().find('[data-cy=myRank]').click();
cy.clientDisplay().contains(rankDetailsTitle);
cy.clientDisplay().find('[data-cy=back]').should('not.exist');
cy.reload();
cy.clientDisplay().contains(rankDetailsTitle);
// click the browser back button and verify that we are still in the
// client display (Subject page)
cy.wait(1000);
cy.go('back') // browser back button
cy.wait(1000);
cy.clientDisplay().contains('Subject 0');
// then back one more time and we should be back on the client display home page
cy.wait(1000);
cy.go('back') // browser back button
cy.wait(1000);
cy.clientDisplay().contains('User Skills');
});
it('route change is passed to the client app', () => {
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
cy.visit('/native/clientDisplay.html?internalBackButton=true');
cy.get('[data-cy=skillsDisplayPath]').contains('Skills Display Path: [/]');
// to subject page
cy.cdClickSubj(0, 'Subject 0');
cy.get('[data-cy=skillsDisplayPath]').contains('Skills Display Path: [/subjects/subj0]');
// navigate to Rank Overview and that it does NOT contains the internal back button
cy.clientDisplay().find('[data-cy=myRank]').click();
cy.clientDisplay().contains(rankDetailsTitle);
cy.get('[data-cy=skillsDisplayPath]').contains('Skills Display Path: [/subjects/subj0/rank]');
});
}
if (Utils.skillsServiceVersionLaterThan('1.6.0')) {
it('navigate skills-display programatically', () => {
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
cy.visit('/native/clientDisplay.html?internalBackButton=true');
cy.get('[data-cy=skillsDisplayPath]').contains('Skills Display Path: [/]');
// to subject page
cy.get('[data-cy=navigateButton]').click();
cy.get('[data-cy=skillsDisplayPath]').contains('Skills Display Path: [/subjects/subj0]');
});
}
});
@@ -0,0 +1,271 @@
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Utils from "./Utils";
import selectors from "../support/selectors";
context("Navigation Tests", () => {
it('internal back button when when returning from an external page', () => {
cy.onlyOn(Utils.skillsClientJSVersionLaterThan('3.3.0'))
cy.createDefaultTinyProject()
cy.intercept(Cypress.env('tokenUrl')).as('getToken')
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
const url = Utils.skillsClientJSVersionLaterThan('3.3.0') ? '?internalBackButton=true' : ''
cy.visitSkillsDisplay(url);
// cy.clientDisplay(true)
cy.wrapIframe().find(selectors.myRankButton)
cy.wrapIframe().find(selectors.titleSection).find(selectors.backButton).should('not.exist')
// navigate to Rank Overview that contains the back button
cy.wrapIframe().find(selectors.myRankButton).click();
cy.wrapIframe().find(selectors.titleSection).contains(Utils.rankDetailsTitle());
cy.wrapIframe().find(selectors.titleSection).find(selectors.backButton).should('exist')
// now visit the "Report Skills" (external) page
cy.get('[data-cy=reportSkillsLink]').click()
cy.contains('Report Skills Examples');
// switch back to the the client display
cy.get('[data-cy=userDisplayLink]').click()
});
it('internal back button when when returning from an external page - multiple layers deep', () => {
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
const url = Utils.skillsClientJSVersionLaterThan('3.3.0') ? '?internalBackButton=true' : ''
cy.visitSkillsDisplay(url);
cy.clientDisplay().find(selectors.myRankButton)
cy.clientDisplay().find(selectors.titleSection).find(selectors.backButton).should('not.exist')
// to subject page
cy.cdClickSubj();
// navigate to Rank Overview that contains the back button
cy.clientDisplay().find(selectors.myRankButton).click();
cy.clientDisplay().find(selectors.titleSection).contains(Utils.rankDetailsTitle());
cy.clientDisplay().find(selectors.backButton).should('exist');
// click the back button and verify that we are still in the
// client display (Subject page)
cy.cdBack('Subject 0');
// then back one more time and we should be back on the client display home page
cy.cdBack('User Skills');
});
it('browser back button works correctly when internal back button is not present', () => {
cy.onlyOn(Utils.skillsClientJSVersionLaterThan('3.3.0'))
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
cy.visit("/native/clientDisplay.html?internalBackButton=false");
cy.clientDisplay().find(selectors.myRankButton)
cy.clientDisplay().find(selectors.titleSection).find(selectors.backButton).should('not.exist')
// to subject page
cy.cdClickSubj();
cy.clientDisplay().find(selectors.titleSection).find(selectors.backButton).should('not.exist')
// navigate to Rank Overview and that it does NOT contains the internal back button
cy.clientDisplay().find(selectors.myRankButton).click()
cy.clientDisplay().find(selectors.titleSection).contains(Utils.rankDetailsTitle());
cy.clientDisplay().find(selectors.titleSection).find(selectors.backButton).should('not.exist')
// click the browser back button and verify that we are still in the
// client display (Subject page)
cy.wait(1000);
cy.go('back') // browser back button
cy.wait(1000);
cy.clientDisplay().find(selectors.titleSection).contains('Subject 0');
// then back one more time and we should be back on the client display home page
cy.wait(1000);
cy.go('back') // browser back button
cy.wait(1000);
cy.clientDisplay().find(selectors.titleSection).contains('User Skills');
});
if (Utils.skillsServiceVersionLaterThan('3.1.1')) {
it('browser back and forward operations', () => {
cy.onlyOn(Utils.skillsClientJSVersionLaterThan('3.3.0'))
cy.createDefaultTinyProject()
// visit client display
cy.visit("/native/clientDisplay.html");
cy.clientDisplay().find(selectors.myRankButton)
// to subject page
cy.cdClickSubj();
// to skill
cy.wrapIframe().find(selectors.buildSkillLinkSelector('Thor')).click()
cy.wrapIframe().find(selectors.titleSection).contains('Skill Overview')
cy.wrapIframe().find(selectors.buildSkillTitleSelector('Thor'))
// // click next
cy.wrapIframe().find(selectors.nextSkillButton).click()
cy.wrapIframe().find(selectors.buildSkillTitleSelector('IronMan'))
cy.go('back')
cy.wrapIframe().find(selectors.buildSkillTitleSelector('Thor'))
cy.go('back')
cy.wrapIframe().find(selectors.titleSection).contains('Subject 0')
cy.clientDisplay().find(selectors.myRankButton)
cy.go('back')
cy.clientDisplay().find(selectors.titleSection).contains('User Skills');
cy.clientDisplay().find(selectors.myRankButton)
cy.go('forward')
cy.wrapIframe().find(selectors.titleSection).contains('Subject 0')
cy.clientDisplay().find(selectors.myRankButton)
// cy.go('forward')
// cy.wrapIframe().find(selectors.titleSection).contains('Skill Overview')
// cy.wrapIframe().find(selectors.buildSkillTitleSelector('Thor'))
//
// cy.go('back')
// cy.wrapIframe().find(selectors.titleSection).contains('Subject 0')
// cy.clientDisplay().find(selectors.myRankButton)
//
// cy.wrapIframe().find(selectors.myRankButton).click();
// cy.wrapIframe().find(selectors.titleSection).contains(Utils.rankDetailsTitle());
//
// cy.go('back')
// cy.wrapIframe().find(selectors.titleSection).contains('Subject 0')
// cy.clientDisplay().find(selectors.myRankButton)
//
// cy.go('back')
// cy.clientDisplay().find(selectors.titleSection).contains('User Skills');
// cy.clientDisplay().find(selectors.myRankButton)
});
}
it('breadcrumb-based navigation', () => {
cy.onlyOn(Utils.skillsClientJSVersionLaterThan('3.3.0'))
cy.createDefaultTinyProject()
// visit client display
cy.visit("/native/clientDisplay.html?skillsClientDisplayPath=%2Fsubjects%2Fsubj0%2Frank");
cy.wrapIframe().find(selectors.titleSection).contains(Utils.rankDetailsTitle());
cy.wrapIframe().find(selectors.buildBreadcrumbLinkSelector('subj0')).click()
cy.wrapIframe().find(selectors.titleSection).contains('Subject 0')
cy.clientDisplay().find(selectors.myRankButton)
cy.wrapIframe().find(selectors.buildBreadcrumbLinkSelector('Overview')).click()
cy.clientDisplay().find(selectors.titleSection).contains('User Skills');
cy.clientDisplay().find(selectors.myRankButton)
});
it('deep link and reload', () => {
cy.onlyOn(Utils.skillsClientJSVersionLaterThan('3.3.0'))
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// navigate to Rank Overview via direct link
cy.visitSkillsDisplay('?skillsClientDisplayPath=%2Frank#/showSkills');
cy.clientDisplay().find(selectors.titleSection).contains(Utils.rankDetailsTitle());
// reload and confirm we are still on Rank Overview page
cy.reload();
cy.clientDisplay().find(selectors.titleSection).contains(Utils.rankDetailsTitle());
});
it('back button after reload', () => {
cy.onlyOn(Utils.skillsClientJSVersionLaterThan('3.3.0'))
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
cy.visit("/native/clientDisplay.html?internalBackButton=false");
cy.clientDisplay().find(selectors.myRankButton)
cy.clientDisplay().find(selectors.titleSection).contains('User Skills');
cy.clientDisplay().find(selectors.titleSection).find(selectors.backButton).should('not.exist')
// to subject page
cy.cdClickSubj();
// navigate to Rank Overview and that it does NOT contain the internal back button
cy.clientDisplay().find(selectors.myRankButton).click();
cy.clientDisplay().find(selectors.titleSection).contains(Utils.rankDetailsTitle());
cy.clientDisplay().find(selectors.titleSection).find(selectors.backButton).should('not.exist')
cy.reload();
cy.clientDisplay().find(selectors.titleSection).contains(Utils.rankDetailsTitle());
cy.clientDisplay().contains(Utils.rankDetailsTitle());
// click the browser back button and verify that we are still in the
// client display (Subject page)
cy.wait(1000);
cy.go('back') // browser back button
cy.wait(1000);
cy.clientDisplay().find(selectors.titleSection).contains('Subject 0');
// then back one more time and we should be back on the client display home page
cy.wait(1000);
cy.go('back') // browser back button
cy.wait(1000);
cy.clientDisplay().find(selectors.titleSection).contains('User Skills');
});
it('route change is passed to the client app', () => {
cy.onlyOn(Utils.skillsClientJSVersionLaterThan('3.3.0'))
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
cy.visitSkillsDisplay('?internalBackButton=true');
// to subject page
cy.cdClickSubj();
cy.get('[data-cy=skillsDisplayPath]').contains('Skills Display Path: [/subjects/subj0]');
// navigate to Rank Overview and that it does NOT contains the internal back button
cy.clientDisplay().find(selectors.myRankButton).click();
cy.clientDisplay().find(selectors.titleSection).contains(Utils.rankDetailsTitle());
cy.get('[data-cy=skillsDisplayPath]').contains('Skills Display Path: [/subjects/subj0/rank]');
});
if (Utils.skillsServiceVersionLaterThan('3.1.1')) {
it('navigate skills-display programmatically', () => {
cy.onlyOn(Utils.skillsClientJSVersionLaterThan('3.3.1'))
cy.createDefaultTinyProject()
cy.backendPost('/api/projects/proj1/skills/Thor', {userId: Cypress.env('proxyUser'), timestamp: Date.now()})
// visit client display
cy.visitSkillsDisplay('?internalBackButton=true');
cy.clientDisplay().find(selectors.myRankButton)
// to subject page
cy.get('[data-cy=navigateButton]').click();
cy.get('[data-cy=skillsDisplayPath]').contains('Skills Display Path: [/subjects/subj0]');
});
}
});
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,8 @@ const homePage = '/native/index.html'
context("Token timeout JS Tests", () => {
const level1CompleteRegex = /"completed"\s*:\s*\[(?=[\s\S]*?\{[^{}]*?(?:"type"\s*:\s*"Overall"[^{}]*?"level"\s*:\s*1|"level"\s*:\s*1[^{}]*?"type"\s*:\s*"Overall")[^{}]*?\})[\s\S]*?\]/;
if (Cypress.env('timeout') === true || Cypress.env('timeout') === 'true') {
it("report skill with expired token, retry checker will resend with new token", () => {
cy.createDefaultProject();
@@ -28,14 +30,17 @@ context("Token timeout JS Tests", () => {
cy.wait(71000) // wait 71 seconds to ensure the token has expired
cy.get('[data-cy="exampleDirectiveClickEventButton"]').click();
cy.get('pre[data-cy=globalEventResult]').should('be.empty');
// should be empty string or empty JSON object
cy.get('pre[data-cy=globalEventResult]')
.invoke('text')
.should('match', /^\s*$|^\s*\{\s*\}\s*$/);
cy.wait("@getToken", { timeout: 60000 }); // wait ~ another 50 seconds to all for the RetryChecker to resubmit
cy.get('pre[data-cy=globalEventResult]').contains('"skillId": "IronMan"')
cy.get('pre[data-cy=globalEventResult]').contains('"pointsEarned": 50')
cy.get('pre[data-cy=globalEventResult]').contains('"skillApplied": true')
cy.get('pre[data-cy=globalEventResult]').contains(/completed": [[][^]*"type": "Overall",[^]\s*"level": 1/)
cy.get('pre[data-cy=globalEventResult]').contains(level1CompleteRegex)
});
it('skill display - navigate after token expires', () => {
@@ -49,7 +54,7 @@ context("Token timeout JS Tests", () => {
// to subject page
cy.wait(10000) // wait 10 seconds to ensure the token has expired
cy.cdClickSubj(0, 'Subject 0');
cy.cdClickSubj();
});
} else {
@@ -0,0 +1,176 @@
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Utils from "./Utils";
import selectors from "../support/selectors";
context("Transcript Export Tests", () => {
const clean = (text) => {
return text.replace(/\n/g, '')
}
let userName
beforeEach(() => {
userName = Cypress.env('oauthMode') ? 'foo bar (foo)' : 'user1'
const getFileName = (projName) => {
return `./cypress/downloads/${projName} - ${userName} - Transcript.pdf`
}
Cypress.Commands.add("readTranscript", (pathToPdf) => {
cy.log(`Reading transcript [${pathToPdf}]`)
cy.readFile(pathToPdf, { timeout: 10000 }).then(() => {
// file exists and was successfully read
cy.log(`Transcript [${pathToPdf}] Found!`)
})
return cy.task('readPdf', pathToPdf)
});
Cypress.Commands.add("readTranscriptForProj", (projName) => {
const pathToPdf = getFileName(projName)
return cy.readTranscript(pathToPdf)
});
})
if (Utils.skillsServiceVersionLaterThan('3.1.0')) {
const expectedHeaderAndFooter = 'For All Dragons enjoyment'
const expectedHeaderAndFooterCommunityProtected = 'For Divine Dragon enjoyment'
it('export user transcript', () => {
const projName = 'Export1'
cy.createDefaultTinyProject('proj1', projName)
// visit client display
cy.visitSkillsDisplay();
cy.clientDisplay().find(selectors.titleSection).contains('User Skills');
cy.clientDisplay().find(selectors.myRankButton)
cy.wrapIframe().find('[data-cy="downloadTranscriptBtn"]').click()
cy.readTranscriptForProj(projName).then((doc) => {
expect(doc.numpages).to.equal(2)
expect(clean(doc.text)).to.include('SkillTree Transcript')
expect(clean(doc.text)).to.include(projName)
expect(clean(doc.text)).to.include('Level: 0 / 5 ')
expect(clean(doc.text)).to.include('Points: 0 / 100 ')
expect(clean(doc.text)).to.include('Skills: 0 / 2 ')
expect(clean(doc.text)).to.include(userName)
expect(clean(doc.text)).to.not.include('Badges')
// should be a title on the 2nd page
expect(clean(doc.text)).to.include('Subject: Subject 0'.toUpperCase())
expect(clean(doc.text)).to.not.include(expectedHeaderAndFooter)
expect(clean(doc.text)).to.not.include(expectedHeaderAndFooterCommunityProtected)
expect(clean(doc.text)).to.not.include('null')
})
});
it('export user transcript - community header', () => {
cy.intercept('GET', '/public/config', (req) => {
req.reply((res) => {
const conf = res.body;
conf.exportHeaderAndFooter = 'For {{community.project.descriptor}} enjoyment';
res.send(conf);
});
}).as('getPublicConfig');
const projName = 'Export2'
cy.createDefaultTinyProject('proj1', projName)
// visit client display
cy.visitSkillsDisplay();
cy.wait('@getPublicConfig')
cy.clientDisplay().find(selectors.titleSection).contains('User Skills');
cy.clientDisplay().find(selectors.myRankButton)
cy.wrapIframe().find('[data-cy="downloadTranscriptBtn"]').click()
cy.readTranscriptForProj(projName).then((doc) => {
expect(doc.numpages).to.equal(2)
expect(clean(doc.text)).to.include('SkillTree Transcript')
expect(clean(doc.text)).to.include(projName)
expect(clean(doc.text)).to.include('Level: 0 / 5 ')
expect(clean(doc.text)).to.include('Points: 0 / 100 ')
expect(clean(doc.text)).to.include('Skills: 0 / 2 ')
expect(clean(doc.text)).to.include(userName)
expect(clean(doc.text)).to.not.include('Badges')
// should be a title on the 2nd page
expect(clean(doc.text)).to.include('Subject: Subject 0'.toUpperCase())
expect(clean(doc.text)).to.include(expectedHeaderAndFooter)
expect(clean(doc.text)).to.not.include(expectedHeaderAndFooterCommunityProtected)
expect(clean(doc.text)).to.not.include('null')
})
});
if (!Cypress.env('oauthMode')) {
it('export user transcript - community protected header', () => {
cy.intercept('GET', '/public/config', (req) => {
req.reply((res) => {
const conf = res.body;
conf.exportHeaderAndFooter = 'For {{community.project.descriptor}} enjoyment';
res.send(conf);
});
}).as('getPublicConfig');
const projName = 'Export3'
cy.createDefaultTinyProject('proj1', projName)
const allDragonsUser = '[email protected]'
cy.fixture('vars.json').then((vars) => {
cy.backendLogout();
cy.backendRegister('user1', vars.defaultPass);
cy.backendLogin(vars.rootUser, vars.defaultPass, true);
cy.backendPost(`/root/users/user1/tags/dragons`, {tags: ['DivineDragon']});
cy.backendPost(`/root/users/${vars.defaultUser}/tags/dragons`, {tags: ['DivineDragon']});
cy.backendLogout();
cy.backendRegister(allDragonsUser, vars.defaultPass);
cy.backendLogout();
cy.backendLogin(vars.defaultUser, vars.defaultPass);
});
cy.backendPost(`/admin/projects/proj1`, {
projectId: 'proj1',
name: projName,
enableProtectedUserCommunity: true
})
// visit client display
cy.visitSkillsDisplay();
cy.wait('@getPublicConfig')
cy.clientDisplay().find(selectors.titleSection).contains('User Skills');
cy.clientDisplay().find(selectors.myRankButton)
cy.wrapIframe().find('[data-cy="downloadTranscriptBtn"]').click()
const pathToPdf = `cypress/downloads/${projName} - Firstname LastName (user1) - Transcript.pdf`
cy.readTranscript(pathToPdf).then((doc) => {
expect(doc.numpages).to.equal(2)
expect(clean(doc.text)).to.include('SkillTree Transcript')
expect(clean(doc.text)).to.include(projName)
expect(clean(doc.text)).to.include('Level: 0 / 5 ')
expect(clean(doc.text)).to.include('Points: 0 / 100 ')
expect(clean(doc.text)).to.include('Skills: 0 / 2 ')
expect(clean(doc.text)).to.include(userName)
expect(clean(doc.text)).to.not.include('Badges')
// should be a title on the 2nd page
expect(clean(doc.text)).to.include('Subject: Subject 0'.toUpperCase())
expect(clean(doc.text)).to.not.include(expectedHeaderAndFooter)
expect(clean(doc.text)).to.include(expectedHeaderAndFooterCommunityProtected)
expect(clean(doc.text)).to.not.include('null')
})
});
}
}
});
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
const { readPdf } = require('./read-pdf')
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
@@ -39,4 +40,8 @@ module.exports = (on, config) => {
}
return launchOptions;
});
on("task", {
readPdf
})
}
@@ -0,0 +1,29 @@
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const fs = require('fs')
const pdf = require('pdf-parse')
const readPdf = (filename) => {
const dataBuffer = fs.readFileSync(filename)
return pdf(dataBuffer).then(function (data) {
return {
numpages: data.numpages,
text: data.text,
}
})
}
module.exports = { readPdf }
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,32 +38,96 @@
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
import selectors from './selectors.js'
const backend = 'http://localhost:8080';
const baseUrl = Cypress.config().baseUrl;
const skillsDisplayHomePage = '/native/clientDisplay.html'
// cy.request does not add Axios's X-XSRF-TOKEN header automatically. Keep the
// cookie and header in sync for requests to the Skills Service, including
// registration and login requests made before the browser visits the dashboard.
Cypress.Commands.overwrite('request', (originalFn, ...args) => {
const options = typeof args[0] === 'object'
? { ...args[0] }
: typeof args[1] === 'string'
? { method: args[0], url: args[1], body: args[2] }
: { url: args[0], body: args[1] };
const method = (options.method || 'GET').toUpperCase();
const url = new URL(options.url, Cypress.config('baseUrl'));
const backend = new URL(Cypress.config('baseUrl'));
const isSkillsService = url.origin === backend.origin ||
(url.hostname === backend.hostname && url.port === '8080');
if (!isSkillsService || ['GET', 'HEAD', 'OPTIONS', 'TRACE'].includes(method)) {
return originalFn(...args);
}
return cy.getCookie('XSRF-TOKEN').then((cookie) => {
if (cookie) {
return originalFn({
...options,
headers: { ...options.headers, 'X-XSRF-TOKEN': cookie.value },
});
}
// The first request can be registration, logout, or login. A GET loads
// the token without requiring an authenticated session.
return cy.request({ url: `${url.origin}/app/userInfo` })
.then(() => cy.getCookie('XSRF-TOKEN').should('exist'))
.then((xsrfCookie) => originalFn({
...options,
headers: { ...options.headers, 'X-XSRF-TOKEN': xsrfCookie.value },
}));
});
});
Cypress.Commands.add('onlyOn', (enabled) => {
if (enabled !== true) {
cy.state('runnable').ctx.skip()
}
})
Cypress.Commands.add("visitSkillsDisplay", (url = '') => {
cy.visit(`${skillsDisplayHomePage}${url}`);
})
Cypress.Commands.add("backendRegister", (user, pass, grantRoot) => {
return cy.request(`${backend}/app/users/validExistingDashboardUserId/${user}`)
.then((response) => {
if (response.body !== true) {
cy.log(`Creating user [${user}]`)
cy.request('PUT', `${backend}/createAccount`, {
const userBody = {
firstName: 'Firstname',
lastName: 'LastName',
email: user,
password: pass,
});
if (grantRoot) {
cy.request('POST', `${backend}/grantFirstRoot`);
}
cy.request('POST', `${backend}/logout`);
if (grantRoot) {
cy.log(`Creating root user [${user}]`)
cy.request('PUT', `${backend}/createRootAccount`, userBody)
} else {
cy.log(`Creating user [${user}]`)
cy.request('PUT', `${backend}/createAccount`, userBody);
}
cy.backendLogout()
} else {
cy.log(`User [${user}] already exists`)
}
});
});
Cypress.Commands.add("backendLogout", () => {
return cy.request('POST', `${backend}/logout`);
})
Cypress.Commands.add("backendPost", (url) => {
return cy.request('POST', `${backend}/${url}`);
})
Cypress.Commands.add("backendLogin", (user, pass) => {
cy.request({
return cy.request({
method: 'POST',
url: `${backend}/performLogin`,
body: {
@@ -75,12 +139,12 @@ Cypress.Commands.add("backendLogin", (user, pass) => {
});
Cypress.Commands.add("backendLogout", () => {
cy.request('POST', `${backend}/logout`);
return cy.request('POST', `${backend}/logout`).as('request')
});
Cypress.Commands.add("backendPost", (url, body) => {
cy.request('POST', `${backend}${url}`, body)
return cy.request('POST', `${backend}${url}`, body)
});
Cypress.Commands.add("reportSkill", (skillId, projId = 'proj1') => {
@@ -107,11 +171,11 @@ Cypress.Commands.add("backendAddSkill", (skillId, version = 0, projId = 'proj1',
})
});
Cypress.Commands.add("createDefaultProject", (numSubj = 3, numSkillsPerSubj = 2, pointIncrement = 50, numPerformToCompletion = 1, projId = 'proj1') => {
Cypress.Commands.add("createDefaultProject", (numSubj = 3, numSkillsPerSubj = 2, pointIncrement = 50, numPerformToCompletion = 1, projId = 'proj1', projName = null) => {
const skillIdsToCreate = ['IronMan', 'Thor']
cy.backendPost(`/app/projects/${projId}`, {
projectId: projId,
name: `Very Cool Project with id ${projId}`
name: projName || `Very Cool Project with id ${projId}`
}).then((resp => {
if (Cypress.env('oauthMode')) {
// if we're in oauthMode, then the foo-hyrda user will own the project. so we need to give the
@@ -140,8 +204,8 @@ Cypress.Commands.add("createDefaultProject", (numSubj = 3, numSkillsPerSubj = 2,
}
});
Cypress.Commands.add("createDefaultTinyProject", (projId = 'proj1') => {
cy.createDefaultProject(1, 2, 50, 1, projId)
Cypress.Commands.add("createDefaultTinyProject", (projId = 'proj1', projName = null) => {
cy.createDefaultProject(1, 2, 50, 1, projId, projName)
});
Cypress.Commands.add("iframe", (handleIframeBody) => {
@@ -167,20 +231,18 @@ Cypress.Commands.add('visitHomePage', (homepage) => {
cy.skillsLog(`Visit Home page for test [${Cypress.mocha.getRunner().test.title}]`)
});
Cypress.Commands.add("cdClickSubj", (subjIndex, expectedTitle) => {
cy.wrapIframe().find(`.user-skill-subject-tile:nth-child(${subjIndex + 1})`).first().click();
if (expectedTitle) {
cy.wrapIframe().contains(expectedTitle);
}
Cypress.Commands.add("cdClickSubj", () => {
cy.wrapIframe().find(selectors.firstSubjectTileBtn).click();
cy.wrapIframe().find(selectors.titleSection).contains('Subject 0');
});
Cypress.Commands.add("cdBack", (expectedTitle = 'User Skills') => {
cy.wrapIframe().find('[data-cy=back]').click()
cy.wrapIframe().find(selectors.backButton).click()
cy.wrapIframe().contains(expectedTitle);
// back button should not exist on the home page, whose title is the default value
if (expectedTitle === 'User Skills') {
cy.wrapIframe().find('[data-cy=back]').should('not.exist');
cy.wrapIframe().find(selectors.backButton).should('not.exist');
}
});
@@ -211,18 +273,23 @@ Cypress.Commands.add('skillsLog', (message) => {
});
});
Cypress.Commands.add('loginBySingleSignOn', (projId = 'proj1') => {
Cypress.Commands.add('loginBySingleSignOn', () => {
Cypress.log({
name: 'loginBySingleSignOn',
})
// first try to get a skills token,
cy.request({
url: `http://localhost:8080/api/projects/${projId}/token`,
// Check the session without saving a protected request for Spring to replay
// after OAuth login, before the test has created any projects.
return cy.request({
url: `${backend}/app/userInfo`,
failOnStatusCode: false,
}).then((tokenResp) => {
if (tokenResp.status === 401) {
cy.skillsLog('Skills token request failed, authenticating with OAuth provider...');
followRedirect: false,
}).then((sessionResp) => {
const requiresLogin = sessionResp.status === 401 ||
(sessionResp.status === 200 && !sessionResp.body?.userId) ||
([301, 302, 303, 307, 308].includes(sessionResp.status) && Boolean(sessionResp.headers.location));
if (requiresLogin) {
cy.skillsLog('Skills session requires authentication, authenticating with OAuth provider...');
cy.request({
url: 'http://localhost:8080/oauth2/authorization/hydra',
qs: { skillsRedirectUri: baseUrl, },
@@ -234,10 +301,14 @@ Cypress.Commands.add('loginBySingleSignOn', (projId = 'proj1') => {
const $html = Cypress.$(resp.body)
const authenticityToken = $html.find('input[name=_csrf]').val()
const challenge = $html.find('input[name=challenge]').val()
expect(authenticityToken, 'OAuth login CSRF token').to.be.a('string').and.not.be.empty;
expect(challenge, 'OAuth login challenge').to.be.a('string').and.not.be.empty;
const options = {
method: 'POST',
url: 'http://localhost:3000/login',
form: true, // we are submitting a regular form body
followRedirect: false,
failOnStatusCode: false,
body: {
_csrf: authenticityToken,
challenge,
@@ -249,38 +320,53 @@ Cypress.Commands.add('loginBySingleSignOn', (projId = 'proj1') => {
};
cy.request(options).then((resp2) => {
expect(resp2.status).to.eq(200)
expect(
resp2.status,
`OAuth login: ${JSON.stringify({
location: resp2.headers.location,
body: resp2.body,
})}`,
).to.eq(302);
if (resp2.redirects[resp2.redirects.length-1].includes('/consent?consent_challenge')) {
cy.skillsLog('Granting consent with OAuth provider...');
const $html = Cypress.$(resp2.body)
const authenticityToken = $html.find('input[name=_csrf]').val()
const challenge = $html.find('input[name=challenge]').val()
// const consentUrl = resp2.redirects.filter(r => r.includes('/consent?consent_challenge'))[0].split(' ')[1]
const options = {
method: 'POST',
url: 'http://localhost:3000/consent',
form: true, // we are submitting a regular form body
qs: { consent_challenge: challenge },
body: {
_csrf: authenticityToken,
challenge,
grant_scope: 'openid',
// grant_scope: 'offline',
submit: 'Allow access',
remember: '1',
},
failOnStatusCode: false,
};
expect(resp2.headers.location, 'OAuth login redirect').to.be.a('string').and.not.be.empty;
const redirectUrl = new URL(resp2.headers.location, options.url).href;
return cy.request(redirectUrl).then((resp2) => {
expect(resp2.status).to.eq(200)
cy.request(options).then((resp3) => {
expect(resp3.status).to.eq(200)
})
}
if ((resp2.redirects || []).some(redirect => redirect.includes('/consent?consent_challenge')) &&
Cypress.$(resp2.body).find('input[name=challenge]').length > 0) {
cy.skillsLog('Granting consent with OAuth provider...');
const $html = Cypress.$(resp2.body)
const authenticityToken = $html.find('input[name=_csrf]').val()
const challenge = $html.find('input[name=challenge]').val()
const options = {
method: 'POST',
url: 'http://localhost:3000/consent',
form: true, // we are submitting a regular form body
qs: { consent_challenge: challenge },
body: {
_csrf: authenticityToken,
challenge,
grant_scope: 'openid',
submit: 'Allow access',
remember: '1',
},
failOnStatusCode: false,
};
cy.request(options).then((resp3) => {
expect(resp3.status).to.eq(200)
})
}
})
})
})
} else {
cy.skillsLog('Received Skills token, already authenticated with OAuth provider.');
expect(sessionResp.status, 'Skills session response status').to.eq(200)
cy.skillsLog('Skills session already authenticated with OAuth provider.');
}
})
});
}).then(() => cy.request(`${backend}/app/userInfo`).then((resp) => {
expect(resp.status, 'Authenticated Skills session status').to.eq(200)
expect(resp.body.userId, 'Authenticated Skills user').to.be.a('string').and.not.be.empty;
}))
});
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,20 +33,26 @@ import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
before(() => {
cy.readFile('../skills-int-client-js/package.json').then((packageJson) => {
const skillsClientJsVersion = packageJson['dependencies']['@skilltree/skills-client-js']
Cypress.env('skills-client-js-version', skillsClientJsVersion)
cy.log(`Running with skills-client-js version [${skillsClientJsVersion}]`)
})
})
beforeEach(() => {
// first call to npm fails, looks like this may be the bug: https://github.com/cypress-io/cypress/issues/6081
cy.exec('npm version', { failOnNonZeroExit: false })
cy.exec('npm run backend:resetDb')
cy.log(`Running with skills-client-js version [${Cypress.env('skills-client-js-version')}]`)
cy.fixture('vars.json').then((vars) => {
cy.request('api/config').then(resp => {
cy.backendRegister(vars.rootUser, vars.defaultPass, true);
cy.backendRegister(vars.defaultUser, vars.defaultPass); // used by the skills-int-service app
const oauthMode = resp.body.authenticator === 'hydra' || resp.body.authenticator === 'http://localhost:8080/oauth2/authorization/hydra'
Cypress.env('oauthMode', oauthMode)
if (!oauthMode) {
if (!Cypress.env('oauthMode')) {
cy.backendLogin(vars.defaultUser, vars.defaultPass);
Cypress.env('proxyUser', 'user1')
Cypress.env('tokenUrl', '/api/users/user1/token')
@@ -0,0 +1,70 @@
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Utils from "../e2e/Utils";
const buildFirstSubjectTileBtnSelector = () => {
if (Utils.skillsServiceVersionLaterThan('3.0.0')) {
return '[data-cy="subjectTile-subj0"] [data-cy="subjectTileBtn"]'
}
return '[data-cy="subjectTile"]:nth-child(1)'
}
const buildMyRankBtnSelector = () => {
if (Utils.skillsServiceVersionLaterThan('3.0.0')) {
return '[data-cy="myRankBtn"]'
}
return '[data-cy="myRank"]'
}
const buildSkillLinkSelector = (skillId) => {
if (Utils.skillsServiceVersionLaterThan('3.0.0')) {
return `[data-cy="skillProgressTitle-${skillId}"] [data-cy="skillProgressTitle"]`
}
return `#skillRow-${skillId} [data-cy="skillProgressTitle"]`
}
const buildSkillTitleSelector = (skillId) => {
if (Utils.skillsServiceVersionLaterThan('3.0.0')) {
return `[data-cy="skillProgressTitle-${skillId}"]`
}
return `#skillProgressTitle-${skillId}`
}
const buildBreadcrumbLinkSelector = (itemId) => {
if (Utils.skillsServiceVersionLaterThan('3.0.0')) {
return `[data-cy="breadcrumbLink-${itemId}"]`
}
return`[data-cy="breadcrumb-${itemId}"]`
}
const selectors = {
backButton: '[data-cy=back]',
myRankButton: buildMyRankBtnSelector(),
titleSection: '[data-cy="skillsTitle"]',
nextSkillButton: '[data-cy="nextSkill"]',
firstSubjectTileBtn: buildFirstSubjectTileBtnSelector(),
buildSkillLinkSelector: buildSkillLinkSelector,
buildSkillTitleSelector: buildSkillTitleSelector,
buildBreadcrumbLinkSelector: buildBreadcrumbLinkSelector
}
export default selectors
@@ -1,4 +1,4 @@
-- Copyright 2020 SkillTree
-- Copyright 2025 SkillTree
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
@@ -12,7 +12,20 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
-- creating Inception project is expensive so lets not delete it
delete from PROJECT_DEFINITION where PROJECT_ID <> 'Inception';
delete from USER_ATTRS where USER_ID <> '[email protected]' and USER_ID <> '[email protected]';
delete from GLOBAL_BADGE_LEVEL_DEFINITION;
delete from SKILL_DEFINITION where PROJECT_ID is null;
delete from project_definition where project_id <> 'Inception';
delete from quiz_definition;
delete from user_attrs where user_id <> '[email protected]' and user_id <> '[email protected]';
delete from user_roles where user_id = '[email protected]' and (role_name = 'ROLE_SUPER_DUPER_USER' OR role_name = 'ROLE_SUPERVISOR' OR role_name = 'ROLE_PROJECT_ADMIN' OR role_name = 'ROLE_PRIVATE_PROJECT_USER' OR role_name = 'ROLE_DASHBOARD_ADMIN_ACCESS');
delete from user_roles where user_id = '[email protected]' and role_name = 'ROLE_SUPERVISOR';
delete from global_badge_level_definition;
delete from skill_definition where project_id is null;
delete from user_achievement;
delete from settings where setting_group <> 'project.inception';
delete from user_points where project_id = 'Inception';
delete from user_achievement where project_id = 'Inception';
delete from user_performed_skill where project_id = 'Inception';
delete from user_events where project_id = 'Inception';
delete from user_tags;
delete from notifications;
delete from user_actions_history;
delete from custom_icons;
@@ -12,11 +12,11 @@
"licenseCheck": "npm run licenseHeaderCheck && npm run licenseDepCheck",
"test": "echo \"Error: no test specified\" && exit 1",
"cy:open": "cypress open",
"cy:open:oauth": "cypress open --env oauthMode=true",
"cy:run": "npm run licenseCheck && cypress run",
"cy:run:oauth": "npm run licenseCheck && cypress run --env authMode=oauth",
"cy:run:oauth": "npm run licenseCheck && cypress run --env oauthMode=true",
"cy:run:timeout": "npm run licenseCheck && cypress run --env timeout=true --spec \"cypress/e2e/timeout.specs.js\"",
"cy:open:dev:js": "cypress open --config baseUrl=http://localhost:8092",
"cy:open:dev:ng": "cypress open --config baseUrl=http://localhost:4200",
"cy:open:dev": "cypress open --config baseUrl=http://localhost:8092",
"integration:start": "java -Dspring.config.location=classpath:/application.properties,file:serverConfigs/integration_application.properties -jar ../skills-int-service/target/skills-int-service-*.jar",
"integration:start:oauth": "java -Dskills.service.authenticator=hydra -Dspring.config.location=classpath:/application.properties,file:serverConfigs/integration_application.properties -jar ../skills-int-service/target/skills-int-service-*.jar",
"integration:start:background": "mkdir -p ./target/logs && npm run integration:start > ./target/logs/integration-apps.out &",
@@ -48,12 +48,12 @@
"dev:removeNpmLinks": "mvn --batch-mode clean package && java -cp target/skills-int-e2e-test-*.jar -Dloader.main=skills.RemoveNpmLinks org.springframework.boot.loader.launch.PropertiesLauncher"
},
"devDependencies": {
"cypress": "13.6.3",
"license-check-and-add": "3.0.4",
"cypress": "14.5.2",
"license-check-and-add": "4.0.5",
"license-checker": "25.0.1",
"semver": "7.3.2",
"wait-on": "5.2.0",
"wait-port": "0.2.9"
},
"dependencies": {}
"pdf-parse": "1.1.1",
"semver": "7.8.5",
"wait-on": "9.1.0",
"wait-port": "1.1.0"
}
}
@@ -5,13 +5,13 @@
<parent>
<groupId>skills</groupId>
<artifactId>skills-client-integration</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>3.6.5-SNAPSHOT</version>
</parent>
<artifactId>skills-int-e2e-test</artifactId>
<description>End to End tests for Skills client libs using skills-int-service app</description>
<properties>
<groovy.version>4.0.7</groovy.version>
<!-- <groovy.version>4.0.7</groovy.version>-->
</properties>
<dependencies>
<dependency>
@@ -41,7 +41,7 @@
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.8.0</version>
<version>2.14.0</version>
</dependency>
<dependency>
<groupId>org.zeroturnaround</groupId>
@@ -104,7 +104,6 @@
<!-- mvn license:add-third-party-->
<groupId>org.codehaus.mojo</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>2.0.0</version>
<executions>
<execution>
<id>default-cli</id>
@@ -133,6 +132,7 @@
<includedLicense>Common Development and Distribution License 1.1 (CDDL-1.1) + The GNU General Public License (GPL) Version 2 with the Classpath Exception</includedLicense>
<includedLicense>Eclipse Public License - Version 2.0</includedLicense>
<includedLicense>The GNU General Public License (GPL) Version 2 with the Classpath Exception</includedLicense>
<includedLicense>GNU Lesser General Public License</includedLicense>
</includedLicenses>
<licenseMerges>
<licenseMerge>The Apache Software License, Version 2.0|Apache License, Version 2.0</licenseMerge>
@@ -144,7 +144,9 @@
<licenseMerge>The 3-Clause BSD License|New BSD License</licenseMerge>
<licenseMerge>Eclipse Public License - Version 1.0|Eclipse Public License - v 1.0</licenseMerge>
<licenseMerge>MIT License|The MIT License</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|EPL 2.0</licenseMerge>
<licenseMerge>MIT License|MIT</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|EPL 2.0|EPL-2.0</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|Eclipse Public License - v 2.0</licenseMerge>
<licenseMerge>The GNU General Public License (GPL) Version 2 with the Classpath Exception|GPL2 w/ CPE</licenseMerge>
</licenseMerges>
</configuration>
@@ -1,4 +1,4 @@
# Copyright 2020 SkillTree
# Copyright 2025 SkillTree
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -14,7 +14,15 @@
spring.datasource.username = postgres
spring.datasource.url = jdbc:postgresql://localhost:5432/skills
spring.datasource.password = skillsPassword
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
skills.config.ui.userCommunityUserTagKey=dragons
skills.config.ui.userCommunityUserTagValue=DivineDragon
skills.config.ui.userCommunityParagraphValidationRegex=^(?i)(?s)((?!divinedragon).)*$
skills.config.ui.userCommunityParagraphValidationMessage=May not contain divinedragon word
skills.config.ui.userCommunityRestrictedDescriptor=Divine Dragon
skills.config.ui.userCommunityBeforeLabel=For
skills.config.ui.userCommunityAfterLabel=Nation
skills.config.ui.defaultCommunityDescriptor=All Dragons
logging.level.org.springframework.security.web.access.intercept.FilterSecurityInterceptor=DEBUG
logging.level.org.springframework.security.web.access.expression=DEBUG
@@ -29,3 +37,5 @@ logging.level.skills=DEBUG
skills.config.client.loggingEnabled=true
skills.config.client.loggingLevel=TRACE
skills.config.client.loggingMaxRequestsPerMinute=5000
@@ -1,4 +1,4 @@
# Copyright 2020 SkillTree
# Copyright 2025 SkillTree
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -17,4 +17,15 @@ spring.security.oauth2.client.registration.hydra.client-secret: client-secret
spring.security.oauth2.client.registration.hydra.scope: openid
spring.security.oauth2.client.registration.hydra.clientName: Hydra
spring.security.oauth2.client.registration.hydra.iconClass: fas fa-key
spring.security.oauth2.client.provider.hydra.issuer-uri: http://localhost:4444/
spring.security.oauth2.client.provider.hydra.issuer-uri: http://localhost:4444/
skills.config.ui.userCommunityUserTagKey=dragons
skills.config.ui.userCommunityUserTagValue=DivineDragon
skills.config.ui.userCommunityParagraphValidationRegex=^(?i)(?s)((?!divinedragon).)*$
skills.config.ui.userCommunityParagraphValidationMessage=May not contain divinedragon word
skills.config.ui.userCommunityRestrictedDescriptor=Divine Dragon
skills.config.ui.userCommunityBeforeLabel=For
skills.config.ui.userCommunityAfterLabel=Nation
skills.config.ui.defaultCommunityDescriptor=All Dragons
skills.config.client.loggingMaxRequestsPerMinute=5000
@@ -1,4 +1,4 @@
# Copyright 2020 SkillTree
# Copyright 2025 SkillTree
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -103,12 +103,5 @@ class ReleaseClientLibs {
json.dependencies.findAll { it.key.startsWith(Vars.NpmNamespace) }.each {
assert json.dependencies."${it.key}" == newVersion
}
if (npmProj.angularModule) {
File loc = new File(npmProj.loc, "projects/skilltree/skills-client-ng/")
assert loc.exists()
NpmProj angularProj = new NpmProj(loc: loc, name: loc.name, doOthersLinkToMe: true)
updateVersion(angularProj, newVersion)
}
}
}
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,111 +0,0 @@
/*
Copyright 2020 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package skills
//import callStack.profiler.CProf
//import callStack.profiler.Profile
import groovy.util.logging.Slf4j
import org.apache.commons.io.FileUtils
import javax.print.attribute.SetOfIntegerSyntax
import java.nio.file.Files
@Slf4j
class SetupNpmLinks {
static void main(String[] args) {
boolean shouldPrune = args.find({ it.equalsIgnoreCase("--prune") })
new SetupNpmLinks(shouldPrune: shouldPrune).init().doLink()
// log.info("Execution Prof:\n{}", CProf.prettyPrint())
}
// configure
File root = new File("./")
boolean shouldPrune = false
// private
TitlePrinter titlePrinter = new TitlePrinter()
List<NpmProj> projs
SetupNpmLinks init(){
projs = new NpmProjBuilder(loc: root).build()
return this
}
// @Profile
void doLink() {
log.info("Should Prune = [{}]", shouldPrune)
assert projs
new RemoveNpmLinks(shouldPrune: shouldPrune, npmInstall: false).init().removeAnyExistingLinks()
createLinks()
npmLinkToSkills()
install()
validateLinks()
build()
}
// @Profile
private void install() {
titlePrinter.printTitle("install")
projs.each { NpmProj npmProj ->
npmProj.exec("npm install --force")
}
}
// @Profile
private void build() {
titlePrinter.printTitle("build")
projs.each { NpmProj npmProj ->
npmProj.exec("npm run build")
}
}
// @Profile
private void validateLinks() {
titlePrinter.printTitle("validate links")
projs.findAll({ it.hasLinksToOtherProjects }).each { NpmProj npmProj ->
titlePrinter.printSubTitle("validating [${npmProj.getSkillsModulesDir().absolutePath}]")
npmProj.exec("ls -l node_modules/@skilltree/")
npmProj.skillsDependenciesFromPackageJson.each {
File shouldBeSymbolic = new File(npmProj.loc, "node_modules/${it}")
assert shouldBeSymbolic.exists()
assert Files.isSymbolicLink(shouldBeSymbolic.toPath())
}
}
}
// @Profile
private void npmLinkToSkills() {
titlePrinter.printTitle("link")
projs.findAll({ it.hasLinksToOtherProjects }).each { NpmProj npmProj ->
npmProj.skillsDependenciesFromPackageJson.each {
titlePrinter.printSubTitle("Linking module [${it}] in project [${npmProj}]")
npmProj.exec("npm link --force ${it}".toString())
}
}
}
// @Profile
private void createLinks() {
titlePrinter.printTitle("create links")
projs.findAll({ it.doOthersLinkToMe }).each {
titlePrinter.printSubTitle("create link for ${it.loc.name}")
it.exec("ls")
it.exec("node -v")
it.exec("npm link")
}
}
}
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
Copyright 2020 SkillTree
Copyright 2025 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -5,7 +5,7 @@
<parent>
<groupId>skills</groupId>
<artifactId>skills-client-integration</artifactId>
<version>3.5.0-SNAPSHOT</version>
<version>3.6.5-SNAPSHOT</version>
</parent>
<artifactId>skills-int-service</artifactId>
<description>SkillTree Client Lib Integration Tests</description>
@@ -23,6 +23,10 @@
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-restclient</artifactId>
</dependency>
</dependencies>
<build>
@@ -57,7 +61,6 @@
<!-- mvn license:add-third-party-->
<groupId>org.codehaus.mojo</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>2.0.0</version>
<executions>
<execution>
<id>default-cli</id>
@@ -99,8 +102,10 @@
<licenseMerge>Eclipse Public License - Version 1.0|Eclipse Public License 1.0</licenseMerge>
<licenseMerge>The 3-Clause BSD License|New BSD License</licenseMerge>
<licenseMerge>Eclipse Public License - Version 1.0|Eclipse Public License - v 1.0</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|EPL 2.0</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|EPL 2.0|EPL-2.0</licenseMerge>
<licenseMerge>Eclipse Public License - Version 2.0|Eclipse Public License - v 2.0</licenseMerge>
<licenseMerge>The GNU General Public License (GPL) Version 2 with the Classpath Exception|GPL2 w/ CPE</licenseMerge>
<licenseMerge>MIT License|MIT</licenseMerge>
</licenseMerges>
</configuration>
</execution>
@@ -112,7 +117,6 @@
mvn com.mycila:license-maven-plugin:3.0:format-->
<groupId>com.mycila</groupId>
<artifactId>license-maven-plugin</artifactId>
<version>3.0</version>
<configuration>
<header>${basedir}/../license-add/LICENSE-HEADER.txt</header>
<excludes>
@@ -1,5 +1,5 @@
/**
* Copyright 2020 SkillTree
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,10 +15,8 @@
*/
package skills.example;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.http.*;
import org.springframework.http.client.support.BasicAuthenticationInterceptor;
import org.springframework.util.LinkedMultiValueMap;
@@ -27,6 +25,8 @@ import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import skills.example.SkillsExampleApplication.SkillsConfig;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.ObjectMapper;
import java.util.ArrayList;
import java.util.Arrays;
@@ -128,6 +128,9 @@ public class Controller {
private void authIfNecessary() {
if (!skillsConfig.getAuthMode().equalsIgnoreCase("pki")) {
// Seed the session and XSRF-TOKEN before /performLogin, which requires CSRF validation.
// SecurityRestTemplateCustomizer retains the cookies and sends X-XSRF-TOKEN on later requests.
restTemplate.getForEntity(skillsConfig.getServiceUrl() + "/app/userInfo", String.class);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
@@ -1,5 +1,5 @@
/**
* Copyright 2020 SkillTree
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/**
* Copyright 2020 SkillTree
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
package skills.example;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.cookie.BasicCookieStore;
import org.apache.hc.client5.http.cookie.Cookie;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.client5.http.socket.ConnectionSocketFactory;
@@ -23,11 +25,9 @@ import org.apache.hc.client5.http.socket.PlainConnectionSocketFactory;
import org.apache.hc.client5.http.ssl.NoopHostnameVerifier;
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory;
import org.apache.hc.core5.http.config.RegistryBuilder;
import org.apache.hc.core5.ssl.SSLContextBuilder;
import org.apache.hc.core5.ssl.SSLContexts;
import org.apache.hc.core5.ssl.TrustStrategy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.client.RestTemplateCustomizer;
import org.springframework.boot.restclient.RestTemplateCustomizer;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
@@ -47,12 +47,22 @@ public class SecurityRestTemplateCustomizer implements RestTemplateCustomizer {
// not by default keeps track of session. This method is called by the
// RestTemplateBuilder used in the skills.example.Controller constructor
HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory();
HttpClient client = getHttpClient();
BasicCookieStore cookieStore = new BasicCookieStore();
HttpClient client = getHttpClient(cookieStore);
clientHttpRequestFactory.setHttpClient(client);
restTemplate.setRequestFactory(clientHttpRequestFactory);
restTemplate.getInterceptors().add((request, body, execution) -> {
for (Cookie cookie : cookieStore.getCookies()) {
if ("XSRF-TOKEN".equals(cookie.getName())) {
request.getHeaders().set("X-XSRF-TOKEN", cookie.getValue());
break;
}
}
return execution.execute(request, body);
});
}
private HttpClient getHttpClient() {
private HttpClient getHttpClient(BasicCookieStore cookieStore) {
try {
SSLContext sslContext = SSLContexts.createSystemDefault();
HostnameVerifier allowAllHosts = new NoopHostnameVerifier();
@@ -69,6 +79,7 @@ public class SecurityRestTemplateCustomizer implements RestTemplateCustomizer {
return HttpClients.custom()
.useSystemProperties()
.setDefaultCookieStore(cookieStore)
.setConnectionManager(poolingHttpClientConnectionManager)
.build();
} catch (Exception e) {
@@ -1,5 +1,5 @@
/**
* Copyright 2020 SkillTree
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/**
* Copyright 2020 SkillTree
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,15 +18,18 @@ package skills.example.filters;
import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Order(SecurityProperties.DEFAULT_FILTER_ORDER-1)
@Order(VueEntryPointFilter.BEFORE_SECURITY_FILTER_ORDER)
public class VueEntryPointFilter implements Filter {
// -100 (Default Security Order) - 1
public static final int BEFORE_SECURITY_FILTER_ORDER = -101;
@Autowired
private VueEntryPointFilterUtils vueEntryPointFilterUtils;
@@ -1,5 +1,5 @@
/**
* Copyright 2020 SkillTree
/*
* Copyright 2025 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
#
# Copyright 2020 SkillTree
# Copyright 2025 SkillTree
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
-18
View File
@@ -1,18 +0,0 @@
module.exports = {
"extends": "airbnb-base",
"rules": {
"no-underscore-dangle": 0,
"max-len": ["error", {'code': 200}],
},
"globals": {
"XMLHttpRequest": 'readonly',
"document": 'readonly',
"alert": 'readonly',
},
"env": {
"browser": true,
"node": true,
"jasmine": true,
"jest": true,
}
};
+32 -15
View File
@@ -1,5 +1,5 @@
<!---
Copyright 2020 SkillTree
Copyright 2026 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -17,25 +17,42 @@ limitations under the License.
# SkillTree Client Lib - Native JS Lib
<p>
<a href="https://www.npmjs.com/package/@skilltree/skills-client-js">
<img src="https://flat.badgen.net/npm/v/@skilltree/skills-client-js" alt="Current version">
</a>
<a href="https://www.npmjs.com/package/@skilltree/skills-client-js">
<img src="https://flat.badgen.net/npm/dt/@skilltree/skills-client-js" alt="npm downloads">
</a>
<a href="https://www.npmjs.com/package/@skilltree/skills-client-js">
<img src="https://flat.badgen.net/npm/dw/@skilltree/skills-client-js" alt="npm weekly downloads">
</a>
<a href="https://www.npmjs.com/package/@skilltree/skills-client-js">
<img src="https://img.shields.io/npm/l/@skilltree/skills-client-js.svg?sanitize=true" alt="License">
</a>
<img src="https://img.shields.io/npm/d18m/%40skilltree%2Fskills-client-js" alt="npm downloads">
<img src="https://img.shields.io/npm/dw/%40skilltree%2Fskills-client-js" alt="npm weekly downloads">
<img src="https://img.shields.io/npm/l/%40skilltree%2Fskills-client-js" alt="License">
<img src="https://img.shields.io/npm/v/%40skilltree%2Fskills-client-js" alt="Latest Version">
<img alt="npm bundle size" src="https://img.shields.io/bundlephobia/minzip/%40skilltree%2Fskills-client-js">
</p>
SkillTree is an innovative approach to implementing application training. SkillTree's ``skills-client-js`` project facilitates rapid integration with the SkillTree service.
To learn about the SkillTree platform please visit our [Official Documentation](https://code.nsa.gov/skills-docs/).
To learn about the SkillTree platform please visit our [Official Documentation](https://skilltreeplatform.dev/).
These pages provide in-depth guidance on the installation, usage and contribution.
## Library Features
SkillTree client library offers a comprehensive set of features:
- **Seamless navigation and visualization:** Our Skill Display component provides a fully navigable Ranking and Visualization view, making it easy for users to explore and understand their skills.
- **Effortless skill event reporting:** The SkillsReporter JS utility simplifies the process of reporting skill events, ensuring accurate and timely tracking of user progress.
- **Convenient level display:** Our level display component provides a straightforward way to showcase a user's current overall level within the application.
- **Global event handling:** Our JS functions enable applications to register to receive skill events, allowing for real-time notifications and updates. For example, you can use this feature to display encouraging messages when users earn points or complete levels, further enhancing their engagement and motivation.
## Important Note
To enable seamless integration with Skills Display, this library utilizes an iFrame wrapper to retrieve
the necessary views and data from the skills-service application. This deliberate architectural design choice is a key
aspect of the SkillTree Platform, as it minimizes the need for frequent upgrades to the skills-client library.
By decoupling the Skills Display functionality from the skills-client library, we've achieved a significant advantage:
most updates and improvements to Skills Display will be automatically reflected in the skills-client library whenever
the skills-service application is upgraded. This means that skills-client integrators can enjoy the benefits of new
features and enhancements without the need for manual library updates.
As a result, **the skills-client libraries are intentionally designed to be relatively static, with infrequent releases**.
This approach ensures that integrators can rely on a stable and consistent library, while still benefiting from the
ongoing evolution of the SkillTree Platform.
## Installation and Usage
To learn how to install and use this library please visit [SkillTree's Native JS Integration Guide](https://code.nsa.gov/skills-docs/skills-client/js.html).
To learn how to install and use this library please visit [SkillTree's Native JS Integration Guide](https://skilltreeplatform.dev/skills-client/js.html).
+1 -1
View File
@@ -1,4 +1,4 @@
module.exports = {
presets: ['@babel/preset-env'],
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
plugins: ['@babel/plugin-transform-runtime'],
};
+24
View File
@@ -0,0 +1,24 @@
/*
Copyright 2026 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import globals from "globals";
import { defineConfig } from "eslint/config";
import js from "@eslint/js";
export default defineConfig([
{ files: ["**/*.js"], languageOptions: { globals: globals.browser } },
{ files: ["**/*.js"], plugins: { js }, extends: ["js/recommended"] },
]);
@@ -1,4 +1,4 @@
Copyright 2020 SkillTree
Copyright 2026 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+33 -30
View File
@@ -1,6 +1,6 @@
{
"name": "@skilltree/skills-client-js",
"version": "3.5.0",
"version": "3.6.5",
"license": "Apache-2.0",
"description": "SkillTree Client Lib - Native JS Lib",
"main": "dist/skills-client-js.umd.min.js",
@@ -17,43 +17,46 @@
"licenseHeaderCheck": " npx license-check-and-add check -f license-add/license-add-config.json",
"licenseHeaderAdd": " npx license-check-and-add add -f license-add/license-add-config.json",
"licenseCheck": "npm run licenseHeaderCheck && npm run licenseDepCheck ",
"build": "npm run licenseCheck && npm run test && npm run clean && npm run build:umd && npm run build:esm",
"build:umd": "rollup --config --format umd --file dist/skills-client-js.umd.min.js --name SkillsClient",
"build:esm": "rollup --config --format esm --file dist/skills-client-js.esm.min.js",
"deploy-to-examples": "npm run build && cp ./dist/skills-display-js.umd.min.js ../skills-examples/skills-example-client-js/app/assets/js/@skilltree/skills-client-js/dist",
"build": "npm run lint && npm run licenseCheck && npm run test && npm run clean && npm run build:umd && npm run build:esm",
"buildSkipTests": "npm run lint && npm run clean && npm run build:umd && npm run build:esm",
"build:umd": "rollup --config --format umd --file dist/skills-client-js.umd.min.js --name SkillsClient --bundleConfigAsCjs",
"build:esm": "rollup --config --format esm --file dist/skills-client-js.esm.min.js --bundleConfigAsCjs",
"deploy-to-examples": "npm run clean && npm run build:umd && npm run build:esm && rm ../skills-client-integration/skills-int-service/src/main/resources/public/native/assets/js/@skilltree/skills-client-js/dist/* && cp ./dist/* ../skills-client-integration/skills-int-service/src/main/resources/public/native/assets/js/@skilltree/skills-client-js/dist/",
"test": "NODE_ENV=test jest --coverage",
"test-watch": "npx jest --passWithNoTests --coverage --watch",
"prepublishOnly": "npm run build",
"lint": "eslint --ext .js src/**"
"lint": "eslint src/**"
},
"author": "SkillTree Team",
"devDependencies": {
"@babel/cli": "7.11.6",
"@babel/core": "7.11.6",
"@babel/plugin-transform-runtime": "7.13.10",
"@babel/polyfill": "7.11.5",
"@babel/preset-env": "7.11.5",
"@rollup/plugin-commonjs": "15.1.0",
"@rollup/plugin-node-resolve": "9.0.0",
"@rollup/plugin-replace": "2.3.3",
"axios-mock-adapter": "1.22.0",
"babel-jest": "26.5.2",
"eslint": "7.11.0",
"eslint-config-airbnb-base": "14.2.0",
"eslint-plugin-import": "2.22.1",
"jest": "26.5.3",
"license-check-and-add": "3.0.4",
"@babel/cli": "7.28.6",
"@babel/core": "7.29.6",
"@babel/plugin-transform-runtime": "7.29.0",
"@babel/polyfill": "7.12.1",
"@babel/preset-env": "7.29.5",
"@babel/runtime": "7.29.2",
"@eslint/js": "10.0.1",
"@rollup/plugin-babel": "7.1.0",
"@rollup/plugin-commonjs": "29.0.3",
"@rollup/plugin-json": "6.1.0",
"@rollup/plugin-node-resolve": "16.0.3",
"@rollup/plugin-replace": "6.0.3",
"@rollup/plugin-terser": "1.0.0",
"axios-mock-adapter": "2.1.0",
"babel-jest": "30.5.1",
"eslint": "10.10.0",
"globals": "17.12.0",
"jest": "30.5.1",
"jest-environment-jsdom": "30.5.1",
"license-check-and-add": "4.0.5",
"license-checker": "25.0.1",
"regenerator-runtime": "0.13.7",
"rollup": "2.30.0",
"rollup-plugin-babel": "4.4.0",
"rollup-plugin-eslint": "7.0.0",
"rollup-plugin-terser": "7.0.2",
"regenerator-runtime": "0.14.1",
"rollup": "4.63.3",
"xhr-mock": "2.5.1"
},
"dependencies": {
"@stomp/stompjs": "6.1.2",
"axios": "1.7.4",
"@stomp/stompjs": "7.3.0",
"axios": "1.20.0",
"js-logger": "1.6.1",
"postmate": "1.5.2",
"sockjs-client": "1.6.1"
@@ -65,7 +68,7 @@
"User Engagement",
"Rapid Integration"
],
"homepage": "https://code.nsa.gov/skills-docs/",
"homepage": "https://skilltreeplatform.dev/",
"repository": {
"type": "git",
"url": "git+https://github.com/NationalSecurityAgency/skills-client.git"
@@ -73,4 +76,4 @@
"bugs": {
"url": "https://github.com/NationalSecurityAgency/skills-client/issues"
}
}
}
+51
View File
@@ -0,0 +1,51 @@
/*
Copyright 2026 SkillTree
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import terser from '@rollup/plugin-terser';
import babel from '@rollup/plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import { readFileSync } from 'fs';
// import pkg from './package.json';
const pkg = JSON.parse(readFileSync(new URL('./package.json', import.meta.url), 'utf8'));
console.log(`running version ${pkg.version}`);
module.exports = {
input: 'src/index.js',
output: {
// need to pass in format, file (and name if format == umd) via cmd line
sourcemap: true,
},
preserveSymlinks: true,
plugins: [
babel({
babelHelpers: 'runtime',
exclude: 'node_modules/**',
}),
resolve({
browser: true,
}),
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
__skillsClientVersion__: `${pkg.name}-${pkg.version}`,
preventAssignment: true,
}),
commonjs(),
terser(), // comment out to remove minimization
],
};
-33
View File
@@ -1,33 +0,0 @@
import { eslint } from 'rollup-plugin-eslint';
import { terser } from 'rollup-plugin-terser';
import babel from 'rollup-plugin-babel';
import commonjs from '@rollup/plugin-commonjs';
import resolve from '@rollup/plugin-node-resolve';
import replace from '@rollup/plugin-replace';
import pkg from './package.json';
module.exports = {
input: 'src/index.js',
output: {
// need to pass in format, file (and name if format == umd) via cmd line
sourcemap: true,
},
preserveSymlinks: true,
plugins: [
eslint(),
babel({
runtimeHelpers: true,
exclude: 'node_modules/**',
}),
resolve({
browser: true,
}),
replace({
'process.env.NODE_ENV': JSON.stringify('production'),
__skillsClientVersion__: `${pkg.name}-${pkg.version}`,
}),
commonjs(),
terser(), // comment out to remove minimization
],
};
+8 -5
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -58,7 +58,7 @@ export default {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', `${conf.getServiceUrl()}/api/projects/${conf.getProjectId()}/skillsClientVersion`);
xhr.withCredentials = true;
xhr.withCredentials = conf.isPKIMode();
if (!conf.isPKIMode()) {
xhr.setRequestHeader('Authorization', `Bearer ${conf.getAuthToken()}`);
}
@@ -99,6 +99,10 @@ export default {
return typeof authenticator === 'string' && authenticator.startsWith(`${serviceUrl}/oauth2/authorization`);
},
assignWindowLocation(newLocation) {
window.location.assign(newLocation);
},
getAuthenticationToken(authenticator, serviceUrl, projectId, oauthRedirect = false) {
return new Promise((resolve, reject) => {
const isOAuthMode = this.isOAuthMode(authenticator, serviceUrl);
@@ -107,7 +111,6 @@ export default {
xhr.open('GET', authenticator);
} else {
xhr.open('GET', `${serviceUrl}/api/projects/${projectId}/token`);
xhr.withCredentials = true;
}
xhr.onreadystatechange = () => {
@@ -115,9 +118,9 @@ export default {
if (xhr.status !== 200) {
if (isOAuthMode && oauthRedirect && xhr.status === 401) {
// if we get 401 and we are using OAuth, then redirect to the OAuth Provider
const oauthAuthenticator = `${authenticator}?skillsRedirectUri=${window.location}`;
const oauthAuthenticator = `${authenticator}?skillsRedirectUri=${encodeURIComponent(window.location.href)}`;
log.info(`SkillsClient::SkillService::unable to get oAuth token, navigating to [${oauthAuthenticator}]`);
window.location.assign(oauthAuthenticator);
this.assignWindowLocation(oauthAuthenticator);
resolve();
} else {
reject(new Error(`SkillTree: Unable to authenticate using [${authenticator}] endpoint. Response Code=[${xhr.status}].\n
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -66,11 +66,24 @@ const exportObject = {
authenticator,
authToken,
oauthRedirect = false,
enabled = true,
}) {
if (this.isInitialized()) {
log.warn('SkillsConfiguration already initialized.');
return;
}
log.debug(`SkillsConfiguration::configure params serviceUrl=[${serviceUrl}], projectId=[${projectId}], `
+ `authenticator=[${authenticator}], authToken=[${authToken}], oauthRedirect=${oauthRedirect}, enabled=${enabled}`)
this.enabled = enabled
if (!enabled) {
log.useDefaults();
log.setLevel(log.TRACE)
log.info('SkillsConfiguration is disabled.');
return Promise.resolve();
}
if (!this.skillsClientVersion) {
// this will be replaced at build time with the current skills-client-js
// version. extensions of the plain JS client (vue, react, etc), should
@@ -91,7 +104,7 @@ const exportObject = {
this.authenticator = authenticator;
this.authToken = authToken;
skillsService.getServiceStatus(`${this.getServiceUrl()}/public/status`).then((response) => {
return skillsService.getServiceStatus(`${this.getServiceUrl()}/public/status`).then((response) => {
this.status = response.status;
skillsService.configureLogging(this.getServiceUrl(), response);
log.info(`Returned status [${JSON.stringify(response)}]`);
@@ -101,7 +114,7 @@ const exportObject = {
}
if (!this.isPKIMode() && !this.getAuthToken()) {
skillsService.getAuthenticationToken(this.getAuthenticator(), this.getServiceUrl(), this.getProjectId(), oauthRedirect)
return skillsService.getAuthenticationToken(this.getAuthenticator(), this.getServiceUrl(), this.getProjectId(), oauthRedirect)
.then((token) => {
this.setAuthToken(token);
setInitialized(this);
@@ -112,7 +125,6 @@ const exportObject = {
setConfigureWasCalled();
}
}).catch((error) => {
// eslint-disable-next-line no-console
console.error('Error getting service status', error);
setConfigureWasCalled();
initializedResolvers.resolve();
@@ -152,6 +164,10 @@ SkillsConfiguration is a singleton and you only have to do this once. Please see
return this.projectId;
},
isDisabled() {
return !this.enabled;
},
getServiceUrl() {
return this.serviceUrl;
},
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -26,17 +26,15 @@ const skillsClientDisplayPath = 'skillsClientDisplayPath';
const markedForDestruction = [];
export default class SkillsDisplayJS {
/* eslint-disable object-curly-newline */
constructor({ options, theme, version, handleRouteChanged, userId } = {}) {
log.debug(`SkillsClient::SkillsDisplayJS::Constructing with options [${options}], theme [${theme}], version [${version}], userId [${userId}]`);
this._options = { ...{ }, ...options };
this._options = { parentPath: window.location.pathname, ...options };
this._theme = theme;
this._version = version;
this._handleRouteChanged = handleRouteChanged;
this._userId = userId;
}
/* eslint-disable class-methods-use-this */
currentIframeId() {
return uniqueId;
}
@@ -48,6 +46,11 @@ export default class SkillsDisplayJS {
}
attachTo(selectorOrElement) {
if (SkillsConfiguration.isDisabled()) {
log.info('SkillsClient::SkillsDisplayJS::attachTo: SkillsConfiguration is disabled Skills Display will not be initialized');
return;
}
log.info(`SkillsClient::SkillsDisplayJS::attaching to [${selectorOrElement ? selectorOrElement.toString() : selectorOrElement}]`);
let iframeContainer = selectorOrElement;
if (typeof selectorOrElement === 'string') {
@@ -80,6 +83,7 @@ export default class SkillsDisplayJS {
});
const iframe = document.querySelector(`.${className}`);
iframe.setAttribute('style', 'border: 0; height: 100%; width: 100%');
iframe.setAttribute('aria-label', 'SkillTree Skills Display')
iframeContainer.height = 0;
iframeContainer.style.height = '0px';
@@ -195,7 +199,6 @@ export default class SkillsDisplayJS {
errorMessage += ' and that your browser trusts the server\'s certificate';
}
errorMessage += `. skilltree service URL: ${this.configuration.serviceUrl}`;
/* eslint-disable no-console */
console.error(errorMessage);
log.error(`SkillsClient::SkillsDisplayJS::${errorMessage}`, error);
ErrorPageUtils.removeAllChildren(iframeContainer);
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,6 +32,10 @@ export default class SkillsLevelJS {
}
attachTo(selectorOrElement) {
if (SkillsConfiguration.isDisabled()) {
log.info('SkillsLevelJS.js::attachTo: SkillsConfiguration is disabled Level will not be initialized');
return;
}
log.info(`SkillsClient::SkillsLevelJS::attaching to [${selectorOrElement}]`);
let skillLevelElement = selectorOrElement;
if (typeof selectorOrElement === 'string') {
@@ -51,7 +55,7 @@ export default class SkillsLevelJS {
this._projectId = SkillsConfiguration.getProjectId();
log.debug(`SkillsClient::SkillsLevelJS::getting projectId from SkillsConfiguration: [${this._projectId}]`);
}
const requestConfig = { withCredentials: true };
const requestConfig = { withCredentials: SkillsConfiguration.isPKIMode() };
if (!SkillsConfiguration.isPKIMode()) {
const authToken = SkillsConfiguration.getAuthToken();
requestConfig.headers = { Authorization: `Bearer ${authToken}` };
@@ -60,7 +64,7 @@ export default class SkillsLevelJS {
.then((result) => {
this.setSkillLevel(result.data);
SkillsReporter.addSuccessHandler(this.update.bind(this));
log.info(`SkillsClient::SkillsLevelJS::recieved initial skill level [${result.data}]`);
log.info(`SkillsClient::SkillsLevelJS::received initial skill level [${result.data}]`);
});
});
}
+1 -1
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -32,7 +32,6 @@ const callSuccessHandlers = (event) => {
};
const callErrorHandlers = (event) => {
// eslint-disable-next-line no-console
console.error('Error reporting skill:', event);
errorHandlerCache.forEach((it) => it(event));
};
@@ -139,7 +138,7 @@ const reportInternal = (resolve, reject, userSkillId, timestamp, isRetry, retryA
const xhr = new XMLHttpRequest();
xhr.open('POST', `${SkillsConfiguration.getServiceUrl()}/api/projects/${SkillsConfiguration.getProjectId()}/skills/${userSkillId}`);
xhr.withCredentials = true;
xhr.withCredentials = SkillsConfiguration.isPKIMode();
if (!SkillsConfiguration.isPKIMode()) {
xhr.setRequestHeader('Authorization', `Bearer ${SkillsConfiguration.getAuthToken()}`);
}
@@ -182,6 +181,11 @@ const SkillsReporter = {
this.maxRetryQueueSize = maxRetryQueueSize;
this.maxRetryAttempts = maxRetryAttempts;
if (SkillsConfiguration.isDisabled()) {
log.info('SkillsClient::SkillsReporter::configure: SkillsConfiguration is disabled. No reporting will occur');
return;
}
if (retryInterval != null) {
// cancel existing RetryChecker, then start w/ passed in retryInterval
this.cancelRetryChecker();
@@ -191,14 +195,26 @@ const SkillsReporter = {
},
addSuccessHandler(handler) {
if (SkillsConfiguration.isDisabled()) {
log.info('SkillsClient::SkillsReporter::addSuccessHandler: SkillsConfiguration is disabled handler is not added');
return;
}
successHandlerCache.add(handler);
log.info(`SkillsClient::SkillsReporter::added success handler [${handler ? handler.toString() : handler}]`);
log.info(`SkillsClient::SkillsReporter::added success handler. successHandlerCache size [${successHandlerCache.size}]`);
},
addErrorHandler(handler) {
if (SkillsConfiguration.isDisabled()) {
log.info('SkillsClient::SkillsReporter::addErrorHandler: SkillsConfiguration is disabled handler is not added');
return;
}
errorHandlerCache.add(handler);
log.info(`SkillsClient::SkillsReporter::added error handler [${handler ? handler.toString() : handler}]`);
log.info(`SkillsClient::SkillsReporter::added error handler. errorHandlerCache size [${errorHandlerCache.size}]`);
},
reportSkill(userSkillId, timestamp = null, isRetry = false, retryAttempt = undefined) {
if (SkillsConfiguration.isDisabled()) {
log.info(`SkillsClient::SkillsReporter::reportSkill: SkillsConfiguration is disabled. No reporting will occur for skillId=[${userSkillId}]`);
return Promise.resolve();
}
log.info(`SkillsClient::SkillsReporter::reporting skill [${userSkillId}] retryAttempt [${retryAttempt}]`);
SkillsConfiguration.validate();
if (retryIntervalId == null) {
+164 -25
View File
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
*/
import mock from 'xhr-mock';
import SkillsService from '../src/SkillsService';
import log from 'js-logger';
require('@babel/polyfill');
@@ -22,32 +23,14 @@ describe('OAuth auto redirect tests', () => {
const mockServiceUrl = 'http://some.com';
const mockProjectId = 'proj1';
const authEndpoint = `${mockServiceUrl}/oauth2/authorization/gitlab`;
const oldWindowLocation = window.location
beforeAll(() => {
delete window.location
window.location = Object.defineProperties(
{},
{
...Object.getOwnPropertyDescriptors(oldWindowLocation),
assign: {
configurable: true,
value: jest.fn(),
},
},
)
})
afterAll(() => {
// restore `window.location` to the `jsdom` `Location` object
window.location = oldWindowLocation
SkillsService.assignWindowLocation = jest.fn();
})
// replace the real XHR object with the mock XHR object before each test
beforeEach(() => {
mock.setup();
const url = /.*\/api\/projects\/proj1\/skillsClientVersion/;
window.location.assign.mockReset()
SkillsService.assignWindowLocation.mockReset()
});
// put the real XHR object back and clear the mocks after each test
@@ -61,8 +44,24 @@ describe('OAuth auto redirect tests', () => {
await SkillsService.getAuthenticationToken(authEndpoint, mockServiceUrl, mockProjectId, true);
expect(window.location.assign).toHaveBeenCalledTimes(1)
expect(window.location.assign).toHaveBeenCalledWith(`${authEndpoint}?skillsRedirectUri=http://localhost/`)
expect(SkillsService.assignWindowLocation).toHaveBeenCalledTimes(1)
expect(SkillsService.assignWindowLocation).toHaveBeenCalledWith(`${authEndpoint}?skillsRedirectUri=${encodeURIComponent('http://localhost/')}`)
});
it('oauth redirect encodes the current location so it cannot add query parameters', async () => {
const oauthTokenEndpoint = `${mockServiceUrl}/api/projects/${mockProjectId}/token`;
mock.get(oauthTokenEndpoint, (req, res) => res.status(401));
window.history.pushState({}, '', '/page?a=1&skillsRedirectUri=http://evil.example.com/');
const injected = window.location.href;
await SkillsService.getAuthenticationToken(authEndpoint, mockServiceUrl, mockProjectId, true);
const [redirectedTo] = SkillsService.assignWindowLocation.mock.calls[0];
expect(redirectedTo).toBe(`${authEndpoint}?skillsRedirectUri=${encodeURIComponent(injected)}`);
// the injected separators must not survive as separators
expect(redirectedTo.split('?')).toHaveLength(2);
expect(redirectedTo).not.toContain('&skillsRedirectUri=');
});
it('oauth does not auto redirect when oauthRedirect=false', async () => {
@@ -73,7 +72,7 @@ describe('OAuth auto redirect tests', () => {
await SkillsService.getAuthenticationToken(authEndpoint, mockServiceUrl, mockProjectId);
} catch (e) {
expect(e.message).toMatch('Unable to authenticate');
expect(window.location.assign).toHaveBeenCalledTimes(0);
expect(SkillsService.assignWindowLocation).toHaveBeenCalledTimes(0);
}
});
@@ -82,8 +81,148 @@ describe('OAuth auto redirect tests', () => {
mock.get(oauthTokenEndpoint, (req, res) => res.status(200).body('{"access_token": "token1"}'));
const result = await SkillsService.getAuthenticationToken(authEndpoint, mockServiceUrl, mockProjectId, true);
expect(window.location.assign).toHaveBeenCalledTimes(0);
expect(SkillsService.assignWindowLocation).toHaveBeenCalledTimes(0);
expect(result).toMatch('token1');
});
it('send log message', async () => {
const logEndpoint = `${mockServiceUrl}/public/log`;
const testMessage = 'Test log message';
const testLevel = 'ERROR';
// Mock the POST request to the log endpoint
mock.post(logEndpoint, (req, res) => {
// Verify the request body contains expected data
expect(req.body()).toContain('message');
expect(req.body()).toContain(testMessage);
expect(req.body()).toContain(testLevel);
return res.status(200).body('{"success": true}');
});
const result = await SkillsService.sendLogMessage(mockServiceUrl, [testMessage], { level: testLevel });
expect(result).toEqual({ success: true });
});
it('send log message failure', async () => {
const logEndpoint = `${mockServiceUrl}/public/log`;
const testMessage = 'Test log message';
const testLevel = 'ERROR';
// Mock a failed response
mock.post(logEndpoint, (req, res) => res.status(500));
try {
await SkillsService.sendLogMessage(mockServiceUrl, [testMessage], { level: testLevel });
fail('Expected sendLogMessage to throw an error');
} catch (error) {
expect(error.message).toContain('Unable to send client log message');
expect(error.message).toContain('Received status [500]');
}
});
it('configure logging with enabled logging', async () => {
const logEndpoint = `${mockServiceUrl}/public/log`;
const testMessage = 'Test log message';
// Mock the POST request for log message
mock.post(logEndpoint, (req, res) => res.status(200).body('{"success": true}'));
const response = {
clientLib: {
loggingEnabled: 'true',
loggingLevel: 'INFO'
}
};
// Mock console.info to capture log output
const consoleSpy = jest.spyOn(console, 'info').mockImplementation();
SkillsService.configureLogging(mockServiceUrl, response);
// Trigger a log message to test the handler
log.info(testMessage);
// Wait for async log message to be sent
await new Promise(resolve => setTimeout(resolve, 10));
// Check that our test message was logged
expect(consoleSpy.mock.calls.some(call =>
call[0] === testMessage || (Array.isArray(call[0]) && call[0].includes(testMessage))
)).toBe(true);
consoleSpy.mockReset();
});
it('configure logging with unknown level defaults to INFO and logs warning', async () => {
const logEndpoint = `${mockServiceUrl}/public/log`;
const response = {
clientLib: {
loggingEnabled: 'true',
loggingLevel: 'UNKNOWN_LEVEL'
}
};
// Mock the POST request for log message
mock.post(logEndpoint, (req, res) => res.status(200).body('{"success": true}'));
// Spy on the logger's warn method instead of console.warn
const logWarnSpy = jest.spyOn(log, 'warn').mockImplementation();
const consoleSpy = jest.spyOn(console, 'info').mockImplementation();
SkillsService.configureLogging(mockServiceUrl, response);
// Verify the specific warning message was logged
expect(logWarnSpy).toHaveBeenCalledWith(
'SkillsClient::SkillService::Unknown log level [UNKNOWN_LEVEL], defaulting to INFO'
);
// Verify that logging still works after defaulting to INFO
log.info('Test message after defaulting');
await new Promise(resolve => setTimeout(resolve, 10));
logWarnSpy.mockRestore();
consoleSpy.mockRestore();
});
it('reportSkillsClientVersion success', async () => {
const mockConfig = {
getServiceUrl: () => mockServiceUrl,
getProjectId: () => mockProjectId,
isPKIMode: () => false,
getAuthToken: () => 'test-token'
};
const versionEndpoint = `${mockServiceUrl}/api/projects/${mockProjectId}/skillsClientVersion`;
const mockResponse = { success: true, version: '3.6.2' };
// Mock a successful response (200)
mock.post(versionEndpoint, (req, res) => res.status(200).body(JSON.stringify(mockResponse)));
const result = await SkillsService.reportSkillsClientVersion(mockConfig);
expect(result).toEqual(mockResponse);
});
it('reportSkillsClientVersion failure', async () => {
const mockConfig = {
getServiceUrl: () => mockServiceUrl,
getProjectId: () => mockProjectId,
isPKIMode: () => false,
getAuthToken: () => 'test-token'
};
const versionEndpoint = `${mockServiceUrl}/api/projects/${mockProjectId}/skillsClientVersion`;
// Mock a failed response (e.g., 401)
mock.post(versionEndpoint, (req, res) => res.status(401));
try {
await SkillsService.reportSkillsClientVersion(mockConfig);
fail('Expected reportSkillsClientVersion to throw an error');
} catch (error) {
expect(error.message).toBe('Unable to report skillsClientVersion. Received status [401]');
}
});
});
@@ -1,325 +0,0 @@
/*
* Copyright 2020 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import mock from 'xhr-mock';
import SkillsConfiguration from '../../src/config/SkillsConfiguration';
describe('SkillsConfiguration', () => {
const flushPromises = () => new Promise(setImmediate);
const mockStatusResponse = '{"status":"OK","clientLib":{"loggingEnabled":"false","loggingLevel":"DEBUG"}}';
const mockVersionResponse = '{"success":true,"explanation":null}';
let statusReqCount = 0;
// replace the real XHR object with the mock XHR object before each test
beforeEach(() => {
mock.setup();
SkillsConfiguration.logout();
const url = /.*\/api\/projects\/.*\/skillsClientVersion/;
mock.post(url, (req, res) => res.status(200).body(mockVersionResponse));
mock.get(/.*\/public\/status/, (req, res) => {
statusReqCount++;
return res.status(200).body(mockStatusResponse);
});
});
// put the real XHR object back and clear the mocks after each test
afterEach(() => {
SkillsConfiguration.logout();
mock.teardown();
statusReqCount = 0;
});
describe('initialization', () => {
it('exists', () => {
expect(SkillsConfiguration).not.toBeNull();
});
it('throws an error SkillsConfiguration.configure() was never called', () => {
expect(() => {
SkillsConfiguration.validate();
}).toThrow();
});
it('isInitialized is false when error occurs in configure', () => {
expect(() => {
SkillsConfiguration.configure({
serviceUrl: 'http://somewhere',
authenticator: 'pki',
});
// when an error occurs during initialization isInitialized should be false, but wasConfigureCalled should be true
expect(SkillsConfiguration.isInitialized()).toBe(false);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
expect(statusReqCount).toBe(1);
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for projectId=[undefined]');
});
it('calling configure more than once after failure will attempt to configure again', async () => {
expect.assertions(6);
mock.reset();
mock.get(/.*\/public\/status/, (req, res) => {
statusReqCount++;
return res.status(503);
});
const mockAuthenticator = Math.random().toString();
mock.get(mockAuthenticator, (req, res) => res.status(200).body('{"access_token": "token"}'));
const config = {
projectId: Math.random().toString(),
serviceUrl: Math.random().toString(),
authenticator: mockAuthenticator,
authToken: Math.random().toString(),
};
SkillsConfiguration.configure(config);
await flushPromises();
expect(SkillsConfiguration.isInitialized()).toBe(false);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
expect(statusReqCount).toBe(1);
SkillsConfiguration.configure(config);
await flushPromises();
expect(SkillsConfiguration.isInitialized()).toBe(false);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
expect(statusReqCount).toBe(2);
});
it('calling configure more than once without failure will NOT attempt to configure again', async () => {
expect.assertions(6);
const mockAuthenticator = Math.random().toString();
mock.get(mockAuthenticator, (req, res) => res.status(200).body('{"access_token": "token"}'));
const config = {
projectId: Math.random().toString(),
serviceUrl: Math.random().toString(),
authenticator: mockAuthenticator,
authToken: Math.random().toString(),
};
SkillsConfiguration.configure(config);
await flushPromises();
expect(SkillsConfiguration.isInitialized()).toBe(true);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
expect(statusReqCount).toBe(1);
SkillsConfiguration.configure(config);
await flushPromises();
expect(SkillsConfiguration.isInitialized()).toBe(true);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
expect(statusReqCount).toBe(1);
});
it('setAuthToken updates the authToken', async () => {
const mockAuthToken = Math.random() + 1;
SkillsConfiguration.configure({ authToken: mockAuthToken, projectId: 'projectId', serviceUrl: 'http://somewhere' });
await flushPromises();
expect(SkillsConfiguration.getAuthToken()).toBe(mockAuthToken);
const newAuthToken = Math.random() + 2;
SkillsConfiguration.setAuthToken(newAuthToken);
expect(SkillsConfiguration.getAuthToken()).toBe(newAuthToken);
});
describe('initializing', () => {
describe('isPKIMode', () => {
it('isPKIMode is false if authenticator is NOT \'pki\'', async () => {
const mockAuthenticator = Math.random().toString();
mock.get(mockAuthenticator, (req, res) => res.status(200).body('{"access_token": "token"}'));
SkillsConfiguration.configure({
authenticator: mockAuthenticator, projectId: 'proj', serviceUrl: 'http://somewhere',
});
await flushPromises();
expect(SkillsConfiguration.isPKIMode()).toBeFalsy();
});
it('isPKIMode is true if authenticator is \'pki\'', async () => {
const mockAuthenticator = 'pki';
SkillsConfiguration.configure({
authenticator: mockAuthenticator, projectId: 'proj', serviceUrl: 'http://somewhere',
});
await flushPromises();
expect(SkillsConfiguration.isPKIMode()).toBeTruthy();
});
});
it('properly initializes variables', async () => {
expect.assertions(6);
const mockServiceUrl = Math.random().toString();
const mockProjectId = Math.random().toString();
const mockAuthenticator = Math.random().toString();
const mockAuthToken = Math.random().toString();
SkillsConfiguration.configure({
projectId: mockProjectId,
serviceUrl: mockServiceUrl,
authenticator: mockAuthenticator,
authToken: mockAuthToken,
});
await flushPromises();
expect(SkillsConfiguration.getProjectId()).toBe(mockProjectId);
expect(SkillsConfiguration.getServiceUrl()).toBe(mockServiceUrl);
expect(SkillsConfiguration.getAuthenticator()).toBe(mockAuthenticator);
expect(SkillsConfiguration.getAuthToken()).toBe(mockAuthToken);
expect(SkillsConfiguration.isInitialized()).toBe(true);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
});
it('notifies afterConfigure Promisees', (done) => {
let resolveCount = 0;
SkillsConfiguration.afterConfigure().then(() => {
resolveCount++;
});
SkillsConfiguration.afterConfigure().then(() => {
resolveCount++;
expect(resolveCount).toBe(2);
done();
});
SkillsConfiguration.configure({ authenticator: 'pki', projectId: 'proj', serviceUrl: 'http://somewhere' });
});
it('trailing slash and spaces are removed from the serviceUrl', async () => {
const authEndpoint = 'http://auth/';
mock.get(authEndpoint, (req, res) => res.status(200).body('{"access_token": "token"}'));
SkillsConfiguration.configure({
projectId: 'proj',
serviceUrl: ' http://some/ ',
authenticator: authEndpoint,
});
await flushPromises();
expect(SkillsConfiguration.getServiceUrl()).toBe('http://some');
});
});
describe('validation', () => {
it('throws an error if projectId is not set', () => {
expect(() => {
SkillsConfiguration.configure({
serviceUrl: 'http://somewhere',
authenticator: 'pki',
});
// when an error occurs during initialization isInitialized should be false, but wasConfigureCalled should be true
expect(SkillsConfiguration.isInitialized()).toBe(false);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for projectId=[undefined]');
});
it('throws an error if projectId is set to null', () => {
expect(() => {
SkillsConfiguration.configure({
projectId: null,
serviceUrl: 'http://somewhere',
authenticator: 'pki',
});
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for projectId=[null]');
});
it('throws an error if serviceUrl is set to null', () => {
expect(() => {
SkillsConfiguration.configure({
projectId: 'proj',
serviceUrl: null,
authenticator: 'pki',
});
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for serviceUrl=[null]');
});
it('throws an error if serviceUrl is not set', () => {
expect(() => {
SkillsConfiguration.configure({
projectId: 'proj',
authenticator: 'pki',
});
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for serviceUrl=[undefined]');
});
it('throws an error if authenticator is set to null', () => {
expect(() => {
SkillsConfiguration.configure({
projectId: 'proj',
serviceUrl: 'http://some',
authenticator: 'null',
});
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for authenticator=[null]');
});
it('throws an error if authenticator is not set', () => {
expect(() => {
SkillsConfiguration.configure({
projectId: 'proj',
serviceUrl: 'http://some',
});
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for authenticator=[undefined]');
});
it('does not throw an error if everything is set', () => {
const mockProjectId = Math.random().toString();
const mockServiceUrl = Math.random().toString();
const mockAuthenticator = Math.random().toString();
const configObject = {
projectId: mockProjectId,
serviceUrl: mockServiceUrl,
authenticator: mockAuthenticator,
};
SkillsConfiguration.configure(configObject);
expect(() => {
SkillsConfiguration.validate();
}).not.toThrow();
});
});
describe('logout', () => {
it('reinitializes the afterConfigure promise' , () => {
const oldPromise = SkillsConfiguration.afterConfigure();
SkillsConfiguration.logout();
const newPromise = SkillsConfiguration.afterConfigure();
expect(oldPromise).not.toBe(newPromise);
});
it('clears the authToken', async () => {
const mockAuthToken = Math.random().toString();;
SkillsConfiguration.configure({
authToken: mockAuthToken,
projectId: 'proj',
serviceUrl: 'http://some',
authenticator: 'http://auth',
});
await flushPromises();
expect(SkillsConfiguration.getAuthToken()).toBe(mockAuthToken);
SkillsConfiguration.logout();
expect(SkillsConfiguration.getAuthToken()).toBe(null);
});
});
});
});
@@ -0,0 +1,225 @@
/*
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import mock from 'xhr-mock';
import SkillsConfiguration from '../../src/config/SkillsConfiguration';
import log from 'js-logger';
/**
* @jest-environment jsdom
*/
describe('SkillsConfigurationInit', () => {
const flushPromises = () => new Promise(setImmediate);
const mockStatusResponse = '{"status":"OK","clientLib":{"loggingEnabled":"false","loggingLevel":"DEBUG"}}';
const mockVersionResponse = '{"success":true,"explanation":null}';
let statusReqCount = 0;
// replace the real XHR object with the mock XHR object before each test
beforeEach(() => {
mock.setup();
SkillsConfiguration.logout();
const url = /.*\/api\/projects\/.*\/skillsClientVersion/;
mock.post(url, (req, res) => res.status(200).body(mockVersionResponse));
mock.get(/.*\/public\/status/, (req, res) => {
statusReqCount++;
return res.status(200).body(mockStatusResponse);
});
});
// put the real XHR object back and clear the mocks after each test
afterEach(() => {
SkillsConfiguration.logout();
mock.teardown();
statusReqCount = 0;
});
it('exists', () => {
expect(SkillsConfiguration).not.toBeNull();
});
it('throws an error SkillsConfiguration.configure() was never called', () => {
expect(() => {
SkillsConfiguration.validate();
}).toThrow();
});
it('isInitialized is false when error occurs in configure', () => {
expect(() => {
SkillsConfiguration.configure({
serviceUrl: 'http://somewhere',
authenticator: 'pki',
});
// when an error occurs during initialization isInitialized should be false, but wasConfigureCalled should be true
expect(SkillsConfiguration.isInitialized()).toBe(false);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
expect(statusReqCount).toBe(1);
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for projectId=[undefined]');
});
it('calling configure more than once after failure will attempt to configure again', async () => {
expect.assertions(6);
mock.reset();
mock.get(/.*\/public\/status/, (req, res) => {
statusReqCount++;
return res.status(503);
});
const mockAuthenticator = Math.random().toString();
mock.get(mockAuthenticator, (req, res) => res.status(200).body('{"access_token": "token"}'));
const config = {
projectId: Math.random().toString(),
serviceUrl: Math.random().toString(),
authenticator: mockAuthenticator,
authToken: Math.random().toString(),
};
SkillsConfiguration.configure(config);
await new Promise(process.nextTick);
expect(SkillsConfiguration.isInitialized()).toBe(false);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
expect(statusReqCount).toBe(1);
SkillsConfiguration.configure(config);
await new Promise(process.nextTick);
expect(SkillsConfiguration.isInitialized()).toBe(false);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
expect(statusReqCount).toBe(2);
});
it('calling configure more than once without failure will NOT attempt to configure again', async () => {
expect.assertions(6);
const mockAuthenticator = Math.random().toString();
mock.get(mockAuthenticator, (req, res) => res.status(200).body('{"access_token": "token"}'));
const config = {
projectId: Math.random().toString(),
serviceUrl: Math.random().toString(),
authenticator: mockAuthenticator,
authToken: Math.random().toString(),
};
SkillsConfiguration.configure(config);
await new Promise(process.nextTick);
expect(SkillsConfiguration.isInitialized()).toBe(true);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
expect(statusReqCount).toBe(1);
SkillsConfiguration.configure(config);
await new Promise(process.nextTick);
expect(SkillsConfiguration.isInitialized()).toBe(true);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
expect(statusReqCount).toBe(1);
});
it('setAuthToken updates the authToken', async () => {
const mockAuthToken = Math.random() + 1;
SkillsConfiguration.configure({
authToken: mockAuthToken,
projectId: 'projectId',
serviceUrl: 'http://somewhere'
});
await new Promise(process.nextTick);
expect(SkillsConfiguration.getAuthToken()).toBe(mockAuthToken);
const newAuthToken = Math.random() + 2;
SkillsConfiguration.setAuthToken(newAuthToken);
expect(SkillsConfiguration.getAuthToken()).toBe(newAuthToken);
});
it('properly initializes variables', async () => {
expect.assertions(6);
const mockServiceUrl = Math.random().toString();
const mockProjectId = Math.random().toString();
const mockAuthenticator = Math.random().toString();
const mockAuthToken = Math.random().toString();
SkillsConfiguration.configure({
projectId: mockProjectId,
serviceUrl: mockServiceUrl,
authenticator: mockAuthenticator,
authToken: mockAuthToken,
});
await new Promise(process.nextTick);
expect(SkillsConfiguration.getProjectId()).toBe(mockProjectId);
expect(SkillsConfiguration.getServiceUrl()).toBe(mockServiceUrl);
expect(SkillsConfiguration.getAuthenticator()).toBe(mockAuthenticator);
expect(SkillsConfiguration.getAuthToken()).toBe(mockAuthToken);
expect(SkillsConfiguration.isInitialized()).toBe(true);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
});
it('notifies afterConfigure Promisees', (done) => {
let resolveCount = 0;
SkillsConfiguration.afterConfigure().then(() => {
resolveCount++;
});
SkillsConfiguration.afterConfigure().then(() => {
resolveCount++;
expect(resolveCount).toBe(2);
done();
});
SkillsConfiguration.configure({authenticator: 'pki', projectId: 'proj', serviceUrl: 'http://somewhere'});
});
it('trailing slash and spaces are removed from the serviceUrl', async () => {
const authEndpoint = 'http://auth/';
mock.get(authEndpoint, (req, res) => res.status(200).body('{"access_token": "token"}'));
SkillsConfiguration.configure({
projectId: 'proj',
serviceUrl: ' http://some/ ',
authenticator: authEndpoint,
});
await new Promise(process.nextTick);
expect(SkillsConfiguration.getServiceUrl()).toBe('http://some');
});
it('configure with enabled=false exercises disabled logging path', () => {
// Spy on the logger methods to verify they are called
const logUseDefaultsSpy = jest.spyOn(log, 'useDefaults').mockImplementation();
const logSetLevelSpy = jest.spyOn(log, 'setLevel').mockImplementation();
const logInfoSpy = jest.spyOn(log, 'info').mockImplementation();
const result = SkillsConfiguration.configure({
serviceUrl: 'http://test.com',
projectId: 'test-project',
authenticator: 'http://test.com/auth',
enabled: false
});
// Verify the specific lines are exercised
expect(logUseDefaultsSpy).toHaveBeenCalled();
expect(logSetLevelSpy).toHaveBeenCalledWith(log.TRACE);
expect(logInfoSpy).toHaveBeenCalledWith('SkillsConfiguration is disabled.');
// Verify it returns a resolved promise
expect(result).toBeInstanceOf(Promise);
// Clean up spies
logUseDefaultsSpy.mockRestore();
logSetLevelSpy.mockRestore();
logInfoSpy.mockRestore();
});
});
@@ -0,0 +1,74 @@
/*
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import mock from 'xhr-mock';
import SkillsConfiguration from '../../src/config/SkillsConfiguration';
/**
* @jest-environment jsdom
*/
describe('SkillsConfiguration', () => {
const mockStatusResponse = '{"status":"OK","clientLib":{"loggingEnabled":"false","loggingLevel":"DEBUG"}}';
const mockVersionResponse = '{"success":true,"explanation":null}';
let statusReqCount = 0;
// replace the real XHR object with the mock XHR object before each test
beforeEach(() => {
mock.setup();
SkillsConfiguration.logout();
const url = /.*\/api\/projects\/.*\/skillsClientVersion/;
mock.post(url, (req, res) => res.status(200).body(mockVersionResponse));
mock.get(/.*\/public\/status/, (req, res) => {
statusReqCount++;
return res.status(200).body(mockStatusResponse);
});
});
// put the real XHR object back and clear the mocks after each test
afterEach(() => {
SkillsConfiguration.logout();
mock.teardown();
statusReqCount = 0;
});
it('reinitializes the afterConfigure promise', () => {
const oldPromise = SkillsConfiguration.afterConfigure();
SkillsConfiguration.logout();
const newPromise = SkillsConfiguration.afterConfigure();
expect(oldPromise).not.toBe(newPromise);
});
it('clears the authToken', async () => {
const mockAuthToken = Math.random().toString();
;
SkillsConfiguration.configure({
authToken: mockAuthToken,
projectId: 'proj',
serviceUrl: 'http://some',
authenticator: 'http://auth',
});
await new Promise(process.nextTick);
expect(SkillsConfiguration.getAuthToken()).toBe(mockAuthToken);
SkillsConfiguration.logout();
expect(SkillsConfiguration.getAuthToken()).toBe(null);
});
});
@@ -0,0 +1,70 @@
/*
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import mock from 'xhr-mock';
import SkillsConfiguration from '../../src/config/SkillsConfiguration';
/**
* @jest-environment jsdom
*/
describe('SkillsConfigurationPkiMode', () => {
const flushPromises = () => new Promise(setImmediate);
const mockStatusResponse = '{"status":"OK","clientLib":{"loggingEnabled":"false","loggingLevel":"DEBUG"}}';
const mockVersionResponse = '{"success":true,"explanation":null}';
let statusReqCount = 0;
// replace the real XHR object with the mock XHR object before each test
beforeEach(() => {
mock.setup();
SkillsConfiguration.logout();
const url = /.*\/api\/projects\/.*\/skillsClientVersion/;
mock.post(url, (req, res) => res.status(200).body(mockVersionResponse));
mock.get(/.*\/public\/status/, (req, res) => {
statusReqCount++;
return res.status(200).body(mockStatusResponse);
});
});
// put the real XHR object back and clear the mocks after each test
afterEach(() => {
SkillsConfiguration.logout();
mock.teardown();
statusReqCount = 0;
});
it('isPKIMode is false if authenticator is NOT \'pki\'', async () => {
const mockAuthenticator = Math.random().toString();
mock.get(mockAuthenticator, (req, res) => res.status(200).body('{"access_token": "token"}'));
SkillsConfiguration.configure({
authenticator: mockAuthenticator, projectId: 'proj', serviceUrl: 'http://somewhere',
});
await new Promise(process.nextTick);
expect(SkillsConfiguration.isPKIMode()).toBeFalsy();
});
it('isPKIMode is true if authenticator is \'pki\'', async () => {
const mockAuthenticator = 'pki';
SkillsConfiguration.configure({
authenticator: mockAuthenticator, projectId: 'proj', serviceUrl: 'http://somewhere',
});
await new Promise(process.nextTick);
expect(SkillsConfiguration.isPKIMode()).toBeTruthy();
});
});
@@ -0,0 +1,126 @@
/*
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import mock from 'xhr-mock';
import SkillsConfiguration from '../../src/config/SkillsConfiguration';
/**
* @jest-environment jsdom
*/
describe('SkillsConfigurationValidation', () => {
const mockStatusResponse = '{"status":"OK","clientLib":{"loggingEnabled":"false","loggingLevel":"DEBUG"}}';
const mockVersionResponse = '{"success":true,"explanation":null}';
let statusReqCount = 0;
// replace the real XHR object with the mock XHR object before each test
beforeEach(() => {
mock.setup();
SkillsConfiguration.logout();
const url = /.*\/api\/projects\/.*\/skillsClientVersion/;
mock.post(url, (req, res) => res.status(200).body(mockVersionResponse));
mock.get(/.*\/public\/status/, (req, res) => {
statusReqCount++;
return res.status(200).body(mockStatusResponse);
});
});
// put the real XHR object back and clear the mocks after each test
afterEach(() => {
SkillsConfiguration.logout();
mock.teardown();
statusReqCount = 0;
});
it('throws an error if projectId is not set', () => {
expect(() => {
SkillsConfiguration.configure({
serviceUrl: 'http://somewhere',
authenticator: 'pki',
});
// when an error occurs during initialization isInitialized should be false, but wasConfigureCalled should be true
expect(SkillsConfiguration.isInitialized()).toBe(false);
expect(SkillsConfiguration.wasConfigureCalled()).toBe(true);
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for projectId=[undefined]');
});
it('throws an error if projectId is set to null', () => {
expect(() => {
SkillsConfiguration.configure({
projectId: null,
serviceUrl: 'http://somewhere',
authenticator: 'pki',
});
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for projectId=[null]');
});
it('throws an error if serviceUrl is set to null', () => {
expect(() => {
SkillsConfiguration.configure({
projectId: 'proj',
serviceUrl: null,
authenticator: 'pki',
});
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for serviceUrl=[null]');
});
it('throws an error if serviceUrl is not set', () => {
expect(() => {
SkillsConfiguration.configure({
projectId: 'proj',
authenticator: 'pki',
});
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for serviceUrl=[undefined]');
});
it('throws an error if authenticator is set to null', () => {
expect(() => {
SkillsConfiguration.configure({
projectId: 'proj',
serviceUrl: 'http://some',
authenticator: 'null',
});
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for authenticator=[null]');
});
it('throws an error if authenticator is not set', () => {
expect(() => {
SkillsConfiguration.configure({
projectId: 'proj',
serviceUrl: 'http://some',
});
}).toThrow('SkillTree: SkillsConfiguration.configure received invalid parameter for authenticator=[undefined]');
});
it('does not throw an error if everything is set', () => {
const mockProjectId = Math.random().toString();
const mockServiceUrl = Math.random().toString();
const mockAuthenticator = 'pki';
const configObject = {
projectId: mockProjectId,
serviceUrl: mockServiceUrl,
authenticator: mockAuthenticator,
};
SkillsConfiguration.configure(configObject);
expect(() => {
SkillsConfiguration.validate();
}).not.toThrow();
});
});
+5 -1
View File
@@ -1,9 +1,13 @@
const Environment = require('jest-environment-jsdom');
const Environment = require('jest-environment-jsdom').TestEnvironment;
/**
* A custom environment to set the TextEncoder that is required by @stomp/stompjs.
*/
module.exports = class CustomTestEnvironment extends Environment {
constructor(config, context) {
super(config, context);
}
async setup() {
await super.setup();
if (typeof this.global.TextEncoder === 'undefined') {
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@ import axios from 'axios';
import SkillsDisplayJS from '../../src/display/SkillsDisplayJS';
import ErrorPageUtils from '../../src/display/ErrorPageUtils';
import SkillsConfiguration from '../../src/config/SkillsConfiguration';
jest.mock('../../src/display/ErrorPageUtils.js');
jest.mock('postmate');
@@ -80,6 +81,7 @@ describe('SkillsDisplayJS', () => {
let realDocument;
beforeEach(() => {
SkillsConfiguration.enabled = true
realDocument = global.document;
global.document.querySelector = jest.fn();
});
@@ -480,6 +482,7 @@ describe('SkillsDisplayJS', () => {
isSummaryOnly: false,
projectId: mockProjectId,
serviceUrl: mockServiceUrl,
parentPath: '/',
};
mockVersion = `${Math.random()}`;
@@ -495,6 +498,7 @@ describe('SkillsDisplayJS', () => {
authenticator: 'pki',
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
parentPath: '/'
};
const client = new SkillsDisplayJS({
@@ -536,6 +540,7 @@ describe('SkillsDisplayJS', () => {
authenticator: mockAuthenticator,
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
parentPath: '/'
};
const client = new SkillsDisplayJS({
options,
@@ -576,6 +581,7 @@ describe('SkillsDisplayJS', () => {
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
internalBackButton: false,
parentPath: '/'
};
const client = new SkillsDisplayJS({
options,
@@ -616,6 +622,7 @@ describe('SkillsDisplayJS', () => {
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
internalBackButton: true,
parentPath: '/'
};
const client = new SkillsDisplayJS({
options,
@@ -655,6 +662,7 @@ describe('SkillsDisplayJS', () => {
authenticator: mockAuthenticator,
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
parentPath: '/'
};
const client = new SkillsDisplayJS({
options,
@@ -693,7 +701,7 @@ describe('SkillsDisplayJS', () => {
describe('construction', () => {
describe('options', () => {
it('accepts options in the constructor', () => {
const mockOptions = { authenticator: 'option' };
const mockOptions = { authenticator: 'option', parentPath: 'non-default/' };
const client = new SkillsDisplayJS({
options: mockOptions,
});
@@ -702,14 +710,14 @@ describe('SkillsDisplayJS', () => {
it('supports null or undefined being passed', () => {
let client = new SkillsDisplayJS({ options: null });
expect(client.options).toEqual({});
expect(client.options).toEqual({ parentPath: '/' });
client = new SkillsDisplayJS({ options: undefined });
expect(client.options).toEqual({});
expect(client.options).toEqual({ parentPath: '/'} );
});
it('supports empty constructor', () => {
it('supports empty constructor, defaults parentPath to window.location.pathname', () => {
const client = new SkillsDisplayJS();
expect(client.options).toEqual({});
expect(client.options).toEqual({ parentPath: '/'} );
});
it('accepts valid options', () => {
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -68,6 +68,7 @@ describe('authFormTests()', () => {
projectId: mockProjectId,
authenticator: authEndpoint,
});
await new Promise(process.nextTick);
const mockUserSkillId = 'skill1';
const url = `${mockServiceUrl}/api/projects/${mockProjectId}/skills/${mockUserSkillId}`;
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,21 +18,33 @@ import mock from 'xhr-mock';
import SkillsConfiguration from '../../src/config/SkillsConfiguration';
import { SkillsReporter } from '../../src/reporter/SkillsReporter.js';
// These tests exercise HTTP authentication, not the background websocket connection.
jest.mock('@stomp/stompjs');
describe('authPkiTests()', () => {
const mockServiceUrl = 'http://some.com';
const mockProjectId = 'proj1';
SkillsConfiguration.configure({
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
authenticator: 'pki',
// replace the real XHR object with the mock XHR object before each test
beforeEach(async () => {
mock.setup();
SkillsConfiguration.logout();
mock.get(/.*\/public\/status/, (req, res) => res.status(200).body('{"status":"OK","clientLib":{"loggingEnabled":"false","loggingLevel":"DEBUG"}}'));
mock.post(/.*\/api\/projects\/proj1\/skillsClientVersion/, (req, res) => res.status(200).body('{"success":true,"explanation":null}'));
await SkillsConfiguration.configure({
serviceUrl: mockServiceUrl,
projectId: mockProjectId,
authenticator: 'pki',
});
});
// replace the real XHR object with the mock XHR object before each test
beforeEach(() => mock.setup());
// put the real XHR object back and clear the mocks after each test
afterEach(() => mock.teardown());
afterEach(() => {
SkillsReporter.cancelRetryChecker();
window.localStorage.removeItem('skillTreeRetryQueue');
SkillsConfiguration.logout();
mock.teardown();
});
it('successful call in pki mode', async () => {
expect.assertions(2);
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
* Copyright 2020 SkillTree
* Copyright 2026 SkillTree
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -55,6 +55,8 @@ describe('userSkillsService', () => {
});
afterEach(() => {
SkillsReporter.cancelRetryChecker();
jest.restoreAllMocks();
window.XMLHttpRequest = realXMLHttpRequest;
});
@@ -79,9 +81,16 @@ describe('userSkillsService', () => {
expect(MockXMLHttpRequest.setRequestHeader).toHaveBeenCalledWith('Authorization', `Bearer ${SkillsConfiguration.getAuthToken()}`);
});
it('enables withCredentials', () => {
it('disables withCredentials in token mode', () => {
SkillsReporter.reportSkill(mockUserSkillId);
expect(MockXMLHttpRequest.withCredentials).toBeTruthy();
expect(MockXMLHttpRequest.withCredentials).toBe(false);
});
it('enables withCredentials without an Authorization header in PKI mode', () => {
SkillsConfiguration.setAuthToken('pki');
SkillsReporter.reportSkill(mockUserSkillId);
expect(MockXMLHttpRequest.withCredentials).toBe(true);
expect(MockXMLHttpRequest.setRequestHeader).not.toHaveBeenCalledWith('Authorization', expect.anything());
});
it('sends out the ajax request', () => {
@@ -162,7 +171,9 @@ describe('userSkillsService', () => {
MockXMLHttpRequest.onreadystatechange();
promise.finally(() => {
promise.catch(() => {
// error is ok here
}).finally(() => {
expect(handler1).toHaveBeenCalledWith(JSON.parse(mockError));
expect(handler2).toHaveBeenCalledWith(JSON.parse(mockError));
expect(consoleSpy).toHaveBeenCalledWith('Error reporting skill:', JSON.parse(mockError));