ep_openid_connect 3.0.13 → 3.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "daily"
7
+ - package-ecosystem: "npm"
8
+ directory: "/"
9
+ schedule:
10
+ interval: "daily"
11
+ versioning-strategy: "increase"
@@ -0,0 +1,84 @@
1
+ name: Backend Tests
2
+
3
+ # any branch is useful for testing before a PR is submitted
4
+ on:
5
+ workflow_call:
6
+
7
+ jobs:
8
+ withplugins:
9
+ # run on pushes to any branch
10
+ # run on PRs from external forks
11
+ if: |
12
+ (github.event_name != 'pull_request')
13
+ || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id)
14
+ name: with Plugins
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ -
18
+ name: Install libreoffice
19
+ uses: awalsh128/cache-apt-pkgs-action@v1.6.0
20
+ with:
21
+ packages: libreoffice libreoffice-pdfimport
22
+ version: 1.0
23
+ -
24
+ name: Install etherpad core
25
+ uses: actions/checkout@v6
26
+ with:
27
+ repository: ether/etherpad-lite
28
+ path: etherpad-lite
29
+ - uses: actions/setup-node@v4
30
+ name: Install Node.js
31
+ with:
32
+ node-version: 22
33
+ - uses: pnpm/action-setup@v6
34
+ name: Install pnpm
35
+ with:
36
+ version: 10
37
+ run_install: false
38
+ - name: Get pnpm store directory
39
+ shell: bash
40
+ run: |
41
+ echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
42
+ - uses: actions/cache@v5
43
+ name: Setup pnpm cache
44
+ with:
45
+ path: ${{ env.STORE_PATH }}
46
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
47
+ restore-keys: |
48
+ ${{ runner.os }}-pnpm-store-
49
+ -
50
+ name: Checkout plugin repository
51
+ uses: actions/checkout@v6
52
+ with:
53
+ path: plugin
54
+ - name: Remove tests
55
+ working-directory: ./etherpad-lite
56
+ run: rm -rf ./src/tests/backend/specs
57
+ -
58
+ name: Install Etherpad core dependencies
59
+ working-directory: ./etherpad-lite
60
+ run: bin/installDeps.sh
61
+ - name: Build admin SPA
62
+ # Two specs hit GET /admin/* which serves files from
63
+ # `src/templates/admin/`; that directory only exists after the
64
+ # admin Vite app is built. Without this step, the route handler
65
+ # 500s on file-not-found and the `admin access` describe block
66
+ # fails (`adminUser can access /admin/`, `normalUser is unable
67
+ # to access /admin/`). installDeps.sh doesn't build it.
68
+ working-directory: ./etherpad-lite
69
+ run: pnpm --filter admin run build-copy
70
+ - name: Install plugin
71
+ working-directory: ./etherpad-lite
72
+ run: |
73
+ pnpm run plugins i --path ../../plugin
74
+ -
75
+ name: Run the backend tests
76
+ working-directory: ./etherpad-lite/src
77
+ run: |
78
+ shopt -s globstar
79
+ res=$(find ./plugin_packages -path "*/static/tests/backend/specs/*" 2>/dev/null | wc -l)
80
+ if [ $res -eq 0 ]; then
81
+ echo "No backend tests found"
82
+ else
83
+ npx cross-env NODE_ENV=production mocha --import=tsx --timeout 120000 --recursive node_modules/ep_*/static/tests/backend/specs/**
84
+ fi
@@ -0,0 +1,72 @@
1
+ # Publicly credit Sauce Labs because they generously support open source
2
+ # projects.
3
+ name: Frontend Tests
4
+
5
+ on:
6
+ workflow_call:
7
+
8
+ jobs:
9
+ test-frontend:
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ -
14
+ name: Check out Etherpad core
15
+ uses: actions/checkout@v6
16
+ with:
17
+ repository: ether/etherpad-lite
18
+ path: etherpad-lite
19
+ - uses: actions/setup-node@v4
20
+ name: Install Node.js
21
+ with:
22
+ node-version: 22
23
+ - uses: pnpm/action-setup@v6
24
+ name: Install pnpm
25
+ with:
26
+ version: 10
27
+ run_install: false
28
+ - name: Get pnpm store directory
29
+ shell: bash
30
+ run: |
31
+ echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
32
+ - uses: actions/cache@v5
33
+ name: Setup pnpm cache
34
+ with:
35
+ path: ${{ env.STORE_PATH }}
36
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
37
+ restore-keys: |
38
+ ${{ runner.os }}-pnpm-store-
39
+ -
40
+ name: Checkout plugin repository
41
+ uses: actions/checkout@v6
42
+ with:
43
+ path: plugin
44
+ -
45
+ name: Install Etherpad core dependencies
46
+ working-directory: ./etherpad-lite
47
+ run: bin/installDeps.sh
48
+ - name: Install plugin
49
+ working-directory: ./etherpad-lite
50
+ run: |
51
+ pnpm run plugins i --path ../../plugin
52
+ - name: Create settings.json
53
+ working-directory: ./etherpad-lite
54
+ run: cp ./src/tests/settings.json settings.json
55
+ - name: Run the frontend tests
56
+ working-directory: ./etherpad-lite
57
+ shell: bash
58
+ run: |
59
+ pnpm run dev &
60
+ connected=false
61
+ can_connect() {
62
+ curl -sSfo /dev/null http://localhost:9001/ || return 1
63
+ connected=true
64
+ }
65
+ now() { date +%s; }
66
+ start=$(now)
67
+ while [ $(($(now) - $start)) -le 30 ] && ! can_connect; do
68
+ sleep 1
69
+ done
70
+ cd src
71
+ pnpm exec playwright install chromium --with-deps
72
+ pnpm run test-ui --project=chromium
@@ -0,0 +1,121 @@
1
+ # This workflow will run tests using node and then publish a package to the npm registry when a release is created
2
+ # For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
3
+ #
4
+ # Publishing uses npm Trusted Publishing (OIDC) — no NPM_TOKEN secret is
5
+ # required. Each package must have a trusted publisher configured on npmjs.com
6
+ # pointing at this workflow file. See:
7
+ # https://docs.npmjs.com/trusted-publishers
8
+
9
+ name: Node.js Package
10
+
11
+ on:
12
+ workflow_call:
13
+
14
+ jobs:
15
+ publish-npm:
16
+ runs-on: ubuntu-latest
17
+ permissions:
18
+ contents: write # for the atomic version-bump push (branch + tag)
19
+ id-token: write # for npm OIDC trusted publishing
20
+ steps:
21
+ - uses: actions/setup-node@v6
22
+ with:
23
+ # OIDC trusted publishing needs npm >= 11.5.1, which requires
24
+ # Node >= 20.17.0. setup-node's `20` resolves to the latest
25
+ # 20.x, which satisfies that.
26
+ node-version: 20
27
+ registry-url: https://registry.npmjs.org/
28
+ - name: Upgrade npm to >=11.5.1 (required for trusted publishing)
29
+ run: npm install -g npm@latest
30
+ - name: Check out Etherpad core
31
+ uses: actions/checkout@v6
32
+ with:
33
+ repository: ether/etherpad-lite
34
+ - uses: pnpm/action-setup@v6
35
+ name: Install pnpm
36
+ with:
37
+ version: 10
38
+ run_install: false
39
+ - name: Get pnpm store directory
40
+ shell: bash
41
+ run: |
42
+ echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
43
+ - uses: actions/cache@v5
44
+ name: Setup pnpm cache
45
+ with:
46
+ path: ${{ env.STORE_PATH }}
47
+ key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
48
+ restore-keys: |
49
+ ${{ runner.os }}-pnpm-store-
50
+ -
51
+ uses: actions/checkout@v6
52
+ with:
53
+ fetch-depth: 0
54
+ -
55
+ name: Bump version (patch)
56
+ id: bump
57
+ run: |
58
+ LATEST_TAG=$(git describe --tags --abbrev=0) || exit 1
59
+ NEW_COMMITS=$(git rev-list --count "${LATEST_TAG}"..) || exit 1
60
+ # No new commits since the last tag → nothing to publish.
61
+ # Setting `bumped=false` lets the publish step below skip
62
+ # itself instead of trying to republish the existing version
63
+ # (which fails with "cannot publish over previously published
64
+ # versions"). Only manual `gh workflow run` invocations on a
65
+ # tag-current branch hit this path; on:push always sees ≥1
66
+ # new commit because the push event is what triggered us.
67
+ if [ "${NEW_COMMITS}" -le 0 ]; then
68
+ echo "bumped=false" >> "${GITHUB_OUTPUT}"
69
+ exit 0
70
+ fi
71
+ git config user.name 'github-actions[bot]'
72
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
73
+ pnpm i --frozen-lockfile
74
+ # `pnpm version patch` bumps package.json, makes a commit, and creates
75
+ # a `v<new-version>` tag. Capture the new tag name from package.json
76
+ # rather than parsing pnpm's output, which has historically varied.
77
+ # Bump the patch component directly with Node. pnpm/action-setup@v6
78
+ # sometimes installs pnpm 11 pre-releases even when version: 10.x is
79
+ # requested (pnpm/action-setup#225); those pre-releases either skip
80
+ # the git commit/tag or reject --no-git-tag-version as unknown.
81
+ # Doing the bump in Node sidesteps both failure modes.
82
+ NEW_VERSION=$(node -e "const fs=require('fs');const p=require('./package.json');const v=p.version.split('.');v[2]=String(Number(v[2])+1);p.version=v.join('.');fs.writeFileSync('./package.json',JSON.stringify(p,null,2)+'\n');console.log(p.version);")
83
+ NEW_TAG="v${NEW_VERSION}"
84
+ git add package.json
85
+ git commit -m "${NEW_TAG}"
86
+ git tag -a "${NEW_TAG}" -m "${NEW_TAG}"
87
+ # CRITICAL: use --atomic so the branch update and the tag update
88
+ # succeed (or fail) as a single transaction on the server. The old
89
+ # `git push --follow-tags` was non-atomic per ref: if a concurrent
90
+ # publish run won the race, the branch fast-forward would be rejected
91
+ # but the tag push would still land — leaving a dangling tag with no
92
+ # matching commit on the branch. Subsequent runs would then forever
93
+ # try to bump to the same already-existing tag and fail with
94
+ # `tag 'vN+1' already exists`. With --atomic, a rejected branch push
95
+ # rejects the tag push too, and the next workflow tick can retry
96
+ # cleanly against the up-to-date refs.
97
+ git push --atomic origin "${GITHUB_REF_NAME}" "${NEW_TAG}"
98
+ echo "bumped=true" >> "${GITHUB_OUTPUT}"
99
+ # This is required if the package has a prepare script that uses something
100
+ # in dependencies or devDependencies.
101
+ -
102
+ if: steps.bump.outputs.bumped == 'true'
103
+ run: pnpm i
104
+ # `npm publish` must come after `git push` otherwise there is a race
105
+ # condition: If two PRs are merged back-to-back then master/main will be
106
+ # updated with the commits from the second PR before the first PR's
107
+ # workflow has a chance to push the commit generated by `npm version
108
+ # patch`. This causes the first PR's `git push` step to fail after the
109
+ # package has already been published, which in turn will cause all future
110
+ # workflow runs to fail because they will all attempt to use the same
111
+ # already-used version number. By running `npm publish` after `git push`,
112
+ # back-to-back merges will cause the first merge's workflow to fail but
113
+ # the second's will succeed.
114
+ #
115
+ # Use `npm publish` directly (not `pnpm publish`) because OIDC trusted
116
+ # publishing requires npm CLI >= 11.5.1 and `pnpm publish` shells out to
117
+ # whichever `npm` is on PATH; calling `npm` directly avoids any shim
118
+ # ambiguity.
119
+ - name: Publish to npm via OIDC
120
+ if: steps.bump.outputs.bumped == 'true'
121
+ run: npm publish --provenance --access public
@@ -0,0 +1,26 @@
1
+ name: Node.js Package
2
+ on: [push]
3
+
4
+ # id-token: write must be granted here so the reusable npmpublish workflow
5
+ # can request an OIDC token for npm trusted publishing.
6
+ permissions:
7
+ contents: write
8
+ id-token: write
9
+
10
+ jobs:
11
+ backend:
12
+ uses: ./.github/workflows/backend-tests.yml
13
+ secrets: inherit
14
+ frontend:
15
+ uses: ./.github/workflows/frontend-tests.yml
16
+ secrets: inherit
17
+ release:
18
+ if: ${{ github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' }}
19
+ needs:
20
+ - backend
21
+ - frontend
22
+ permissions:
23
+ contents: write # for the version bump push
24
+ id-token: write # for npm OIDC trusted publishing
25
+ uses: ./.github/workflows/npmpublish.yml
26
+ secrets: inherit
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
- ![Publish Status](https://github.com/ether/ep_openid_connect/workflows/Node.js%20Package/badge.svg) ![Backend Tests Status](https://github.com/ether/ep_openid_connect/workflows/Backend%20tests/badge.svg)
1
+ ![Publish Status](https://github.com/ether/ep_openid_connect/workflows/Node.js%20Package/badge.svg) [![Backend Tests Status](https://github.com/ether/ep_openid_connect/actions/workflows/test-and-release.yml/badge.svg)](https://github.com/ether/ep_openid_connect/actions/workflows/test-and-release.yml)
2
2
 
3
- # ep\_openid\_connect
3
+ # OpenID Connect Authentication for Etherpad
4
4
 
5
5
  Etherpad plugin to authenticate users against an OpenID Connect provider.
6
6
 
package/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const Ajv = require('ajv/dist/jtd');
4
4
  const {URL} = require('url');
5
- const {Issuer, generators} = require('ether-node-openid-client');
5
+ const {Issuer, generators} = require('openid-client');
6
6
 
7
7
  let logger = {};
8
8
  for (const level of ['debug', 'info', 'warn', 'error']) {
@@ -30,7 +30,11 @@ const validSettings = new Ajv().compile({
30
30
  user_properties: {values: {
31
31
  optionalProperties: {
32
32
  claim: {type: 'string'},
33
- default: {type: 'string'},
33
+ // `default` is assigned verbatim to `req.session.user[propName]`,
34
+ // so any JSON value (boolean for is_admin/readOnly/canCreate,
35
+ // number, string, …) is fine. Use the JTD empty form so we don't
36
+ // reject non-string defaults (#100).
37
+ default: {},
34
38
  },
35
39
  nullable: true,
36
40
  }},
@@ -271,4 +275,5 @@ exports.preAuthorize = (hookName, {req}) => {
271
275
 
272
276
  exports.exportedForTestingOnly = {
273
277
  defaultSettings,
278
+ validSettings,
274
279
  };
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "ep_openid_connect",
3
- "version": "3.0.13",
3
+ "version": "3.0.20",
4
4
  "description": "Etherpad plugin to authenticate users against an OpenID Connect provider.",
5
5
  "main": "index.js",
6
6
  "repository": {
7
7
  "type": "git",
8
- "url": "https://github.com/ether/ether-plugins.git"
8
+ "url": "https://github.com/ether/ep_openid_connect.git"
9
9
  },
10
10
  "license": "MIT",
11
11
  "keywords": [
@@ -19,30 +19,29 @@
19
19
  "plugin"
20
20
  ],
21
21
  "bugs": {
22
- "url": "https://github.com/ether/ether-plugins/issues"
22
+ "url": "https://github.com/ether/ep_openid_connect/issues"
23
23
  },
24
- "homepage": "https://github.com/ether/ether-plugins/tree/main/ep_openid_connect#readme",
24
+ "homepage": "https://github.com/ether/ep_openid_connect#readme",
25
25
  "dependencies": {
26
- "ether-node-openid-client": "^6.0.2",
27
- "ajv": "^8.11.0",
28
- "openid-client": "^5.1.5"
26
+ "ajv": "^8.18.0",
27
+ "openid-client": "^5.7.1"
29
28
  },
30
29
  "devDependencies": {
31
- "eslint": "^9.24.0",
32
- "eslint-config-etherpad": "^4.0.4",
30
+ "eslint": "^8.57.1",
31
+ "eslint-config-etherpad": "^4.0.5",
33
32
  "oidc-provider": "^8.8.1",
34
- "supertest": "^7.1.0",
35
- "typescript": "^5.4.3"
33
+ "supertest": "^6.3.4",
34
+ "typescript": "^5.9.3"
35
+ },
36
+ "scripts": {
37
+ "lint": "eslint .",
38
+ "lint:fix": "eslint --fix ."
36
39
  },
37
40
  "funding": {
38
41
  "type": "individual",
39
42
  "url": "https://etherpad.org/"
40
43
  },
41
44
  "engines": {
42
- "node": ">=20.0.0"
43
- },
44
- "scripts": {
45
- "lint": "eslint .",
46
- "lint:fix": "eslint --fix ."
45
+ "node": ">=18.0.0"
47
46
  }
48
- }
47
+ }
@@ -1,7 +1,10 @@
1
1
  'use strict';
2
2
 
3
- const MemoryAdapter = require('oidc-provider/lib/adapters/memory_adapter');
4
- const Provider = require('oidc-provider');
3
+ // oidc-provider v8+ is ESM-only (`"type": "module"` in package.json). When
4
+ // Node `require()`s it from a CommonJS module, the returned value is the
5
+ // ESM namespace object, not the class itself — so `.default` is needed.
6
+ const MemoryAdapter = require('oidc-provider/lib/adapters/memory_adapter').default;
7
+ const Provider = require('oidc-provider').default;
5
8
  const common = require('ep_etherpad-lite/tests/backend/common');
6
9
  const http = require('http');
7
10
  const util = require('util');
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ const assert = require('assert').strict;
4
+ const {validSettings} = require('../../../..').exportedForTestingOnly;
5
+
6
+ const baseSettings = () => ({
7
+ client_id: 'cid',
8
+ client_secret: 'secret',
9
+ base_url: 'https://example.test/',
10
+ });
11
+
12
+ describe(__filename, function () {
13
+ it('still accepts a string default on user_properties', function () {
14
+ const s = {
15
+ ...baseSettings(),
16
+ user_properties: {displayname: {claim: 'name', default: 'Anon'}},
17
+ };
18
+ assert(validSettings(s),
19
+ `expected valid settings, got errors: ${JSON.stringify(validSettings.errors)}`);
20
+ });
21
+
22
+ it('accepts a boolean default on user_properties (regression for #100)', function () {
23
+ const s = {
24
+ ...baseSettings(),
25
+ user_properties: {
26
+ is_admin: {claim: 'etherpad_is_admin', default: false},
27
+ readOnly: {default: false},
28
+ canCreate: {default: true},
29
+ },
30
+ };
31
+ assert(validSettings(s),
32
+ `expected boolean defaults to validate, got errors: ` +
33
+ `${JSON.stringify(validSettings.errors)}`);
34
+ });
35
+
36
+ it('accepts a numeric default on user_properties', function () {
37
+ const s = {
38
+ ...baseSettings(),
39
+ user_properties: {quota: {default: 100}},
40
+ };
41
+ assert(validSettings(s),
42
+ `expected numeric defaults to validate, got errors: ` +
43
+ `${JSON.stringify(validSettings.errors)}`);
44
+ });
45
+ });
@@ -7,7 +7,7 @@ const epOpenidConnect = require('../../../../index');
7
7
  const login = require('../login');
8
8
  const padManager = require('ep_etherpad-lite/node/db/PadManager');
9
9
  const plugins = require('ep_etherpad-lite/static/js/pluginfw/plugin_defs');
10
- import settings from 'ep_etherpad-lite/node/utils/Settings'
10
+ const settings = require('ep_etherpad-lite/node/utils/Settings');
11
11
  const supertest = require('supertest');
12
12
 
13
13
  describe(__filename, function () {
@@ -36,10 +36,26 @@ describe(__filename, function () {
36
36
  await common.init();
37
37
  if (!plugins.hooks.clientVars) plugins.hooks.clientVars = [];
38
38
  backup.hooks = {clientVars: plugins.hooks.clientVars};
39
- backup.settings = {...settings};
39
+ // Only capture the fields the tests actually mutate. Etherpad's
40
+ // settings module now exposes several read-only getters (e.g.
41
+ // abiwordAvailable), which made the old `{...settings}` + Object.assign
42
+ // cleanup throw "Cannot set property … of #<Object> which has only a
43
+ // getter".
44
+ backup.settings = {
45
+ requireAuthentication: settings.requireAuthentication,
46
+ requireAuthorization: settings.requireAuthorization,
47
+ users: settings.users,
48
+ };
40
49
  provider = new OidcProvider();
50
+ // `redirect_uris` must be strings, not URL objects. oidc-provider@9.8.3
51
+ // copies the clients config via structuredClone() during init
52
+ // (initialize_clients.js:18), which throws DataCloneError on URL
53
+ // instances ("Cannot clone object of unsupported type"). Earlier
54
+ // versions tolerated URL objects because they relied on a shallow
55
+ // copy. Use `.href` to coerce to the string form the spec actually
56
+ // expects.
41
57
  const clients =
42
- [{...client, redirect_uris: [new URL('/ep_openid_connect/callback', common.baseUrl)]}];
58
+ [{...client, redirect_uris: [new URL('/ep_openid_connect/callback', common.baseUrl).href]}];
43
59
  await provider.start({clients});
44
60
  });
45
61
 
@@ -59,7 +75,11 @@ describe(__filename, function () {
59
75
  if (socket != null) socket.close();
60
76
  socket = null;
61
77
  Object.assign(plugins.hooks, backup.hooks);
62
- Object.assign(settings, backup.settings);
78
+ // Restore only the fields we changed in beforeEach — see note in
79
+ // `before()` about read-only getters on the settings module.
80
+ settings.requireAuthentication = backup.settings.requireAuthentication;
81
+ settings.requireAuthorization = backup.settings.requireAuthorization;
82
+ settings.users = backup.settings.users;
63
83
  });
64
84
 
65
85
  after(async function () {
@@ -183,8 +203,13 @@ describe(__filename, function () {
183
203
  assert.equal(res.status, 200);
184
204
  });
185
205
 
186
- it('normalUser is unable to access /admin/', async function () {
187
- const url = new URL('/admin/', common.baseUrl).toString();
206
+ it('normalUser is unable to access /admin-auth/', async function () {
207
+ // The admin gate moved from `/admin/` to `/admin-auth/*` when core
208
+ // refactored the admin UI into a public SPA — `/admin/` now serves
209
+ // static HTML/JS to anyone, and `webaccess.ts:60` only enforces
210
+ // is_admin for paths starting with `/admin-auth`. Test the gate
211
+ // where it actually lives now.
212
+ const url = new URL('/admin-auth/', common.baseUrl).toString();
188
213
  const res = await login(agent, issuer, url, 'normalUser');
189
214
  assert.equal(res.request.url, url); // Should have been redirected back after authenticating.
190
215
  assert.equal(res.status, 403);
@@ -0,0 +1,13 @@
1
+ import {expect, test} from '@playwright/test';
2
+ import {getPadBody, goToNewPad} from 'ep_etherpad-lite/tests/frontend-new/helper/padHelper';
3
+
4
+ test.beforeEach(async ({page}) => {
5
+ await goToNewPad(page);
6
+ });
7
+
8
+ test.describe('ep_openid_connect', () => {
9
+ test('pad loads with plugin installed', async ({page}) => {
10
+ const padBody = await getPadBody(page);
11
+ await expect(padBody).toBeVisible();
12
+ });
13
+ });