diff --git a/.gitignore b/.gitignore index f330e9f8bc6..bb0b9d2abf0 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,15 @@ Pipfile.lock # ack-grep .ackrc + +# Screenshot automation +node_modules/ +cypress/snapshots/ +cypress/videos/ +cypress/downloads/ +screenshot-inventory.json +cypress/fixtures/pdfs/ +playwright/results/ +playwright/.auth/ +playwright/fixtures/pdfs/ +playwright/fixtures/images/ diff --git a/AGENTS.md b/AGENTS.md index 1a27a47aa0d..5e60c995d89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -171,6 +171,161 @@ gh issue edit NNNN \ Use `fixes #NNNN` in the PR body to auto-close on merge; use `relates to #NNNN` if the PR only partially addresses the issue. +## Screenshot composition + +Rules for Playwright screenshot specs. Refine this section as new patterns emerge. + +### Clip to element bounding box, not container offsets + +Always anchor clips to the target element's own `boundingBox()`, never to hardcoded pixel offsets +from a parent container. Fixed offsets break silently when adjacent UI (badges, notification buttons, +extra rows) shifts position. + +```typescript +const btn = page.locator('button', { hasText: 'Archived conversations' }) +const listEl = page.locator('[aria-label="Conversation list"]') +const listBox = await listEl.boundingBox() +const btnBox = await btn.boundingBox() +if (listBox && btnBox) { + await page.screenshot({ + path: dest, + clip: { + x: listBox.x, + y: btnBox.y - 80, // ~80px above to show context + width: listBox.width, + height: btnBox.height + 88, // button height + ~8px below + }, + }) +} +``` + +### Show menu/list items in context + +When screenshotting a button or item inside a list or panel, include enough surrounding rows to show +where it lives. ~80px above the target is a reasonable default; adjust if nearby rows are unusually +tall. A crop so tight the element appears orphaned gives users no spatial reference. + +### Wait for animation before screenshotting nested panels + +If clicking a button navigates *within* a container (not a separate modal), the replaced section may +still be animating out. Wait for it to reach `state: 'hidden'`, then add a short `waitForTimeout(400)` +to let the incoming panel settle: + +```typescript +await page.locator('button:has-text("Manage bans")').click() +const banDialog = page.getByRole('dialog', { name: /banned users/i }) +await banDialog.waitFor({ state: 'visible', timeout: 10000 }) +await page.locator('#settings-section_conversation-settings') + .waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {}) +await page.waitForTimeout(400) +await banDialog.screenshot({ path: dest }) +``` + +### Use `pressSequentially` for Vue reactive search inputs + +`fill()` sets an input's value in one shot and can bypass Vue's reactive watchers, leaving the UI +in its previous state (e.g. a search field appears empty, results never update). Use +`pressSequentially` with a small inter-key delay so each keystroke fires its own input event: + +```typescript +// fill() bypasses Vue reactivity — use pressSequentially instead +await searchInput.click() +await searchInput.pressSequentially('laptop', { delay: 80 }) +// Confirm reactivity fired: wait for a DOM change only possible after the search updates +await page.locator('.frequently-used-label').waitFor({ state: 'hidden', timeout: 3000 }).catch(() => {}) +await page.locator('.search-result').first().click({ timeout: 3000 }).catch(() => {}) +``` + +### Guard conditional `boundingBox()` calls with `isVisible()` + +`locator.boundingBox()` waits for the element using the full action timeout (default 30–60 s) when the +element is absent from the DOM. An unguarded `.catch(() => null)` does not help — the 60 s timeout fires +before the catch runs. Always check `isVisible()` first (instant, no wait) before calling `boundingBox()`: + +```typescript +// Wrong — times out for 60s if element is absent: +const box = await locator.boundingBox().catch(() => null) + +// Correct — instant check, then fetch box only when present: +const box = (await locator.isVisible()) ? await locator.boundingBox() : null +``` + +This is especially important inside screenshot tests that compute clip regions from optional UI +(e.g. a badge button that only appears under certain conditions). + +### Seed rich content inline between messages + +File shares, reactions, and other message cards must be seeded *between* the surrounding messages +they should appear near. Seeding them before the conversation is populated places them at the top of +chat history, scrolled out of the visible viewport by the time the screenshot is taken. + +```typescript +await seedChatMessages(token, [ /* messages before the share */ ]) +await uploadFile(path, name, user, password) +await ocsRequest('POST', '/ocs/v2.php/apps/files_sharing/api/v1/shares', user, password, { + shareType: '10', path: `/${name}`, shareWith: token, +}) +await seedChatMessages(token, [ /* messages after the share */ ]) +``` + +### Backdating folder mtimes in the Files app + +NC's `oc_filecache.mtime` is what the Files app displays, but you must update **both** the DB and +the real filesystem — NC's lazy scanner re-reads the filesystem on every PROPFIND and will overwrite +a DB-only change. Use `files/FolderName` as the path (the home storage prepends `files/`): + +```typescript +// Update oc_filecache +const sql = `UPDATE oc_filecache SET mtime=${ts}, storage_mtime=${ts} + WHERE path='files/Documents' + AND storage=(SELECT numeric_id FROM oc_storages WHERE id='home::christine')` +// Also touch the real directory +await runExec(['touch', '-m', '-d', `@${ts}`, '/var/www/html/data/christine/files/Documents']) +``` + +Run backdates **after** the first login — apps like Talk initialise their storage folder on first +login and would overwrite an earlier backdate. + +NC's in-process Sabre/DAV cache (per Apache worker, not APCu) holds the mtime from first access +and ignores external writes for the lifetime of that worker. For screenshots that must show a +specific folder date, intercept the PROPFIND response and patch `getlastmodified` directly: + +```typescript +// In beforeEach — intercepts the root directory listing for all tests +await page.route('**/remote.php/dav/files/christine/', async (route, request) => { + if (request.method() !== 'PROPFIND') { await route.continue(); return } + try { + const response = await route.fetch() + const body = await response.text() + const patched = body.replace( + /([^<]*\/Talk\/<\/d:href>[\s\S]*?)(.*?)(<\/d:getlastmodified>)/, + '$1Thu, 15 May 2026 00:00:00 GMT$3', + ) + await route.fulfill({ response, body: patched, contentType: response.headers()['content-type'] || 'application/xml' }) + } catch (_) { + await route.continue().catch(() => {}) + } +}) +``` + +### `oc_talk_rooms.last_activity` is a DATETIME column, not a Unix integer + +Writing a raw integer to this column causes PHP's `new DateTime()` to throw (HTTP 500). Use +SQLite's `datetime()` conversion: + +```sql +UPDATE oc_talk_rooms SET last_activity = datetime(1748822400, 'unixepoch') WHERE token = 'abc' +``` + +Talk's `getRooms` API also filters rooms by `modifiedSince`. Backdating `last_activity` makes rooms +disappear after the first poll. Fix by setting `last_attendee_activity` 2 h in the future — Talk +passes the room if either timestamp is recent enough: + +```sql +UPDATE oc_talk_attendees SET last_attendee_activity = +WHERE room_id = (SELECT id FROM oc_talk_rooms WHERE token = 'abc') AND actor_id = 'christine' +``` + ## CI checks (must all pass) | Check | What it catches | diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000..a95fa1b5c3c --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3953 @@ +{ + "name": "nextcloud-documentation-screenshots", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "nextcloud-documentation-screenshots", + "license": "AGPL-3.0-or-later", + "devDependencies": { + "@nextcloud/e2e-test-server": "^0.4.0", + "@playwright/test": "^1.60.0", + "@types/dockerode": "^4.0.1", + "@types/node": "^20.0.0", + "pngquant-bin": "^9.0.0", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@balena/dockerignore": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", + "integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/tlds": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@nextcloud/e2e-test-server": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@nextcloud/e2e-test-server/-/e2e-test-server-0.4.0.tgz", + "integrity": "sha512-nKdLXOn4TY9+Z/dE4AKDsk6Fhp6xm5gUIFx4gW5z4Ivrp/nl7iGun5zDmbyjW7mHF55orqVxNl8GBHzVDTd0Sg==", + "dev": true, + "license": "AGPL-3.0-or-later", + "dependencies": { + "@nextcloud/paths": "^2.2.1", + "dockerode": "^4.0.2", + "fast-xml-parser": "^5.2.2", + "wait-on": "^9.0.1" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nextcloud/e2e-test-server/node_modules/wait-on": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.10.tgz", + "integrity": "sha512-rCoJEhvMr0X6alHmwc9abbrA5ZrLZFKpFQVKPNFwl2h7DapXOGdmimIHDtLOWhT4PjhZhxFEtZoQgEXbkDWdZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.16.0", + "joi": "^18.2.1", + "lodash": "^4.18.1", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@nextcloud/paths": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@nextcloud/paths/-/paths-2.4.0.tgz", + "integrity": "sha512-35hykjqzaJCw8pBYWuKbLLw2wyKMuf9+T8K8GoYiS84AIi8SO16nuEu0fyl/gwCuiDqX5tCCup4wqljf0hdvaw==", + "dev": true, + "license": "GPL-3.0-or-later", + "engines": { + "node": "^20.0.0 || ^22.0.0 || ^24.0.0" + } + }, + "node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@playwright/test": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", + "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.1.tgz", + "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-4.0.1.tgz", + "integrity": "sha512-cmUpB+dPN955PxBEuXE3f6lKO1hHiIGYJA46IVF3BJpNsZGvtBDcRnlrHYHtOH/B6vtDOyl2kZ2ShAu3mgc27Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/node": { + "version": "20.19.39", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", + "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/archive-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", + "integrity": "sha512-zV4Ky0v1F8dBrdYElwTvQhweQ0P7Kwc1aluqJsYtOBP01jXcWCyW2IEfI1YiqsG+Iy7ZR+o5LF1N+PGECBxHWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^4.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/archive-type/node_modules/file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", + "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/bin-build": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bin-build/-/bin-build-3.0.0.tgz", + "integrity": "sha512-jcUOof71/TNAI2uM5uoUaDq2ePcVBQ3R/qhxAz1rX7UfvduAL/RXD3jXzvn8cVcDJdGVkiR1shal3OH0ImpuhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress": "^4.0.0", + "download": "^6.2.2", + "execa": "^0.7.0", + "p-map-series": "^1.0.0", + "tempfile": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/bin-build/node_modules/execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-build/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-build/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-build/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-build/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/bin-check": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", + "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^0.7.0", + "executable": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "node_modules/bin-check/node_modules/execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-check/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-check/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/bin-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-3.1.0.tgz", + "integrity": "sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^1.0.0", + "find-versions": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-version-check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-4.0.0.tgz", + "integrity": "sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bin-version": "^3.0.0", + "semver": "^5.6.0", + "semver-truncate": "^1.1.2" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-version-check/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/bin-version/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/bin-version/node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-version/node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-version/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-version/node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-version/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-version/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/bin-version/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-version/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/bin-version/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/bin-wrapper": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-wrapper/-/bin-wrapper-4.1.0.tgz", + "integrity": "sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bin-check": "^4.1.0", + "bin-version-check": "^4.0.0", + "download": "^7.1.0", + "import-lazy": "^3.1.0", + "os-filter-obj": "^2.0.0", + "pify": "^4.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-wrapper/node_modules/download": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/download/-/download-7.1.0.tgz", + "integrity": "sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archive-type": "^4.0.0", + "caw": "^2.0.1", + "content-disposition": "^0.5.2", + "decompress": "^4.2.0", + "ext-name": "^5.0.0", + "file-type": "^8.1.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^8.3.1", + "make-dir": "^1.2.0", + "p-event": "^2.1.0", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-wrapper/node_modules/download/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/file-type": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", + "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-wrapper/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/got": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", + "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/got/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/p-cancelable": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/p-event": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", + "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-wrapper/node_modules/p-timeout": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-wrapper/node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/bin-wrapper/node_modules/url-parse-lax": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" + } + }, + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "dev": true, + "license": "MIT" + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-fill": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/buildcheck": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz", + "integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==", + "dev": true, + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/cacheable-request/node_modules/lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", + "integrity": "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "get-intrinsic": "^1.3.0", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/caw": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", + "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-proxy": "^2.0.0", + "isurl": "^1.0.0-alpha5", + "tunnel-agent": "^0.6.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true, + "license": "ISC" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha512-yjLXh88P599UOyPTFX0POsd7WxnbsVsGohcwzHOLspIhhpalPw1BcqED8NblyZLKcGrL8dTgMlcaZxV2jAD41Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cpu-features": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", + "integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "~0.0.6", + "nan": "^2.19.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tar/node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/decompress-tar/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-tar/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-tar/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/decompress-tar/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/decompress-tar/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/decompress-tar/node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-tarbz2/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-targz/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha512-1fqeluvxgnn86MOh66u8FjbtJpAFv5wgCT9Iw8rcBqQcCo5tO8eiJw7NNTrvt9n4CRBVq7CstiS922oPgyGLrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/docker-modem": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-5.0.7.tgz", + "integrity": "sha512-XJgGhoR/CLpqshm4d3L7rzH6t8NgDFUIIpztYlLHIApeJjMZKYJMz2zxPsYxnejq5h3ELYSw/RBsi3t5h7gNTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.1.1", + "readable-stream": "^3.5.0", + "split-ca": "^1.0.1", + "ssh2": "^1.15.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/dockerode/-/dockerode-4.0.12.tgz", + "integrity": "sha512-/bCZd6KlGcjZO8Buqmi/vXuqEGVEZ0PNjx/biBNqJD3MhK9DmdiAuKxqfNhflgDESDIiBz3qF+0e55+CpnrUcw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@balena/dockerignore": "^1.0.2", + "@grpc/grpc-js": "^1.11.1", + "@grpc/proto-loader": "^0.7.13", + "docker-modem": "^5.0.7", + "protobufjs": "^7.3.2", + "tar-fs": "^2.1.4", + "uuid": "^10.0.0" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/dockerode/node_modules/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/download": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/download/-/download-6.2.5.tgz", + "integrity": "sha512-DpO9K1sXAST8Cpzb7kmEhogJxymyVUd5qz/vCOSyvwtp2Klj2XcDt5YUuasgxka44SxF0q5RriKIwJmQHG2AuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "caw": "^2.0.0", + "content-disposition": "^0.5.2", + "decompress": "^4.0.0", + "ext-name": "^5.0.0", + "file-type": "5.2.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^7.0.0", + "make-dir": "^1.0.0", + "p-event": "^1.0.0", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer3": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", + "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-xml-builder": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.7.tgz", + "integrity": "sha512-Yh7/7rQuMXICNr0oMYDR2yHP6oUvmQsTToFeOWj/kIDhAwQ+c4Ol/lbcwOmEM5OHYQmh6S6EQSQ1sljCKP36bQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.2.tgz", + "integrity": "sha512-P7oW7tLbYnhOLQk/Gv7cZgzgMPP/XN03K02/Jy6Y/NHzyIAIpxuZIM/YqAkfiXFPxA2CTm7NtCijK9EDu09u2w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.5", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", + "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/find-versions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", + "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-regex": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/from2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/from2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/from2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "license": "MIT" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-proxy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", + "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "npm-conf": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", + "integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "decompress-response": "^3.2.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-plain-obj": "^1.1.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "p-cancelable": "^0.3.0", + "p-timeout": "^1.1.1", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "url-parse-lax": "^1.0.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/got/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/got/node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbol-support-x": "^1.4.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/import-lazy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", + "integrity": "sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha512-TcdjPibTksa1NQximqep2r17ISRiNE9fwlfbg3F8ANdvP5/yrFTew86VcO//jk4QTaMlbjypPBq76HN2zaKfZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha512-Y4LTamMe0DDQIIAlaer9eKebAlDSV6huy+TWhJVPlzZh2o4tRP5SQWFlLn5N0To4mDD22/qdOq+veo1cSISLgQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/joi": { + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", + "integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/lowercase-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", + "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "license": "ISC", + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nan": { + "version": "2.26.2", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.26.2.tgz", + "integrity": "sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-url/node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/normalize-url/node_modules/sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "dev": true, + "license": "MIT", + "dependencies": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-conf/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-filter-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", + "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "arch": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-cancelable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", + "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-event": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz", + "integrity": "sha512-hV1zbA7gwqPVFcapfeATaNjQ3J0NuzorHPyG8GPL9g/Y/TplWVBVoCKCXL6Ej2zscrCEv195QNWJXuBH6XZuzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^1.1.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha512-zL7VE4JVS2IFSkR2GQKDSPEVxkoH43/p7oEnwpdCndKYJO0HVeRB7fA8TJwuLOTBREtK0ea8eHaxdwcpob5dmg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-map-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", + "integrity": "sha512-4k9LlvY6Bo/1FcIdV33wqZQES0Py+iKISU9Uc8p8AjWoZPnFKMpVIVD3s0EYn4jzLh1I+WeUZkJ0Yoa4Qfw3Kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-reduce": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha512-3Tx1T3oM1xO/Y8Gj0sWyE78EIJZ+t+aEmXUdvQgvGmSMri7aPTHoovbXEreWKkL5j21Er60XAWLTzKbAKYOujQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-timeout": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", + "integrity": "sha512-gb0ryzr+K2qFqFv6qi3khoeqMZF/+ajxQipEF6NteZVnvz9tzdsfAVj3lYtn1gAXvH5lfLwfxEII799gt/mRIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/playwright": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", + "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.60.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.60.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", + "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pngquant-bin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/pngquant-bin/-/pngquant-bin-9.0.0.tgz", + "integrity": "sha512-jlOKfIQBTNJwQn2JKK5xLmwrsi/NwVTmHRvbrknCjdWxfX1/c/+yP4Jmp9jRZWedft/vnhh+rGbvl/kUmesurg==", + "dev": true, + "hasInstallScript": true, + "license": "GPL-3.0+", + "dependencies": { + "bin-build": "^3.0.0", + "bin-wrapper": "^4.0.1", + "execa": "^8.0.1" + }, + "bin": { + "pngquant": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/pngquant-bin/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/pngquant-bin/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngquant-bin/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/pngquant-bin/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngquant-bin/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngquant-bin/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngquant-bin/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngquant-bin/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pngquant-bin/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/pngquant-bin/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/protobufjs": { + "version": "7.5.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.6.tgz", + "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.1", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/pseudomap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^1.0.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } + }, + "node_modules/seek-bzip/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/semver-truncate": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-1.1.2.tgz", + "integrity": "sha512-V1fGg9i4CL3qesB6U0L6XAm4xOJiHmt4QAacazumuasc03BvtFGIMCduv01JWQ69Nv+JST9TqhSCiJoxoY031w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.3.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/semver-truncate/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sort-keys-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-ca": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz", + "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ssh2": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", + "integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "asn1": "^0.2.6", + "bcrypt-pbkdf": "^1.0.2" + }, + "engines": { + "node": ">=10.16.0" + }, + "optionalDependencies": { + "cpu-features": "~0.0.10", + "nan": "^2.23.0" + } + }, + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-natural-number": "^4.0.1" + } + }, + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strnum": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/temp-dir": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", + "integrity": "sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tempfile": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", + "integrity": "sha512-ZOn6nJUgvgC09+doCEF3oB+r3ag7kUvlsXEGX069QRD60p+P3uP7XG9N2/at+EyIRGSN//ZY3LyEotA1YpmjuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "temp-dir": "^1.0.0", + "uuid": "^3.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tempfile/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/timed-out": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", + "integrity": "sha512-G7r3AhovYtr5YKOWQkta8RKAPb+J9IsO4uVmzjl8AZwfhs8UcUwTiD6gcJYSgOtzyjvQKrKYn41syHbUWMkafA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/to-buffer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz", + "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "isarray": "^2.0.5", + "safe-buffer": "^5.2.1", + "typed-array-buffer": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/url-parse-lax": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", + "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "prepend-http": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/url-to-options": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", + "integrity": "sha512-0kQLIzG4fdk/G5NONku64rSH/x32NOA39LVQqlK8Le6lvTF6GGRJpqaQFGgU+CLwySIqBSMdwYM0sYcW9f6P4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000000..2c2fb3e5612 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "nextcloud-documentation-screenshots", + "private": true, + "description": "Screenshot automation for Nextcloud documentation", + "license": "AGPL-3.0-or-later", + "engines": { + "node": ">=20" + }, + "scripts": { + "screenshots": "playwright test", + "screenshots:headed": "playwright test --headed", + "screenshots:ui": "playwright test --ui", + "inventory": "python3 scripts/inventory.py" + }, + "devDependencies": { + "@nextcloud/e2e-test-server": "^0.4.0", + "@playwright/test": "^1.60.0", + "@types/dockerode": "^4.0.1", + "@types/node": "^20.0.0", + "pngquant-bin": "^9.0.0", + "typescript": "^5.0.0" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000000..cba37532f70 --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { defineConfig } from '@playwright/test' + +export const SCREENSHOT_PORT = 8093 +export const BASE_URL = `http://localhost:${SCREENSHOT_PORT}` + +// NC 33 requires Chrome 142+. Headless Chromium ships a lower version string +// by default, which triggers the browser-compatibility warning. Override it here +// so we never need CDP workarounds in individual specs. +const USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36' + +export default defineConfig({ + testDir: './playwright/e2e', + outputDir: './playwright/results', + + timeout: 60_000, + expect: { timeout: 15_000 }, + + // One worker — screenshot automation is not parallel (one Docker container). + workers: 1, + retries: 1, + fullyParallel: false, + + use: { + baseURL: BASE_URL, + viewport: { width: 1440, height: 900 }, + userAgent: USER_AGENT, + storageState: './playwright/.auth/state.json', + video: 'off', + screenshot: 'off', + }, + + projects: [ + { + name: 'chromium', + use: { + channel: 'chromium', + permissions: ['camera', 'microphone'], + launchOptions: { + args: ['--use-fake-ui-for-media-stream', '--use-fake-device-for-media-stream'], + }, + }, + }, + ], + + globalSetup: './playwright/global-setup.ts', + globalTeardown: './playwright/global-teardown.ts', +}) diff --git a/playwright/e2e/user/files.spec.ts b/playwright/e2e/user/files.spec.ts new file mode 100644 index 00000000000..fbfa2e55ac0 --- /dev/null +++ b/playwright/e2e/user/files.spec.ts @@ -0,0 +1,440 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { test, Cookie } from '@playwright/test' +import { User } from '@nextcloud/e2e-test-server' +import { login } from '@nextcloud/e2e-test-server/playwright' +import { runExec } from '@nextcloud/e2e-test-server/docker' +import { + docScreenshot, + docElementScreenshot, + tryOcc, + uploadAvatar, + uploadFile, + mkdavCol, + ocsRequest, +} from '../../helpers' +import * as path from 'path' +import * as fs from 'fs' +import * as os from 'os' +import { execSync } from 'child_process' + +test.describe.configure({ mode: 'serial' }) + +const user = new User('christine', 'christine') + +let authCookies: Cookie[] = [] + +const AVATAR_DIR = '/home/anna/Downloads/tp/avatar' +const WALLPAPERS = '/home/anna/Downloads/wallpapers' +const FIXTURES_PDFS = path.join(__dirname, '../../fixtures/pdfs') + +/** Write a temp file and return its path. */ +function tmpFile(name: string, content: string): string { + const p = path.join(os.tmpdir(), name) + fs.writeFileSync(p, content, 'utf8') + return p +} + +/** Create a .docx via LibreOffice headless conversion from a plain-text source. */ +function createDocx(baseName: string, content: string): string { + const txt = tmpFile(`${baseName}.txt`, content) + execSync(`libreoffice --headless --convert-to docx --outdir "${os.tmpdir()}" "${txt}"`, { timeout: 60000 }) + return path.join(os.tmpdir(), `${baseName}.docx`) +} + +function d(isoDate: string): number { + return Math.floor(new Date(isoDate).getTime() / 1000) +} + +/** + * Backdate a folder's mtime so the Files app shows a realistic date instead of + * "a few seconds ago". Updates both oc_filecache (what NC serves) and the real + * directory on disk (which NC re-reads on page load and would otherwise overwrite + * the DB value). + */ +async function backdateFolderMtime(folderPath: string, user: string, mtime: number): Promise { + // Update oc_filecache — NC stores paths as 'files/' in the home storage + const sql = `UPDATE oc_filecache SET mtime = ${mtime}, storage_mtime = ${mtime} WHERE path = 'files/${folderPath}' AND storage = (SELECT numeric_id FROM oc_storages WHERE id = 'home::${user}')` + const b64 = Buffer.from(sql).toString('base64') + const php = `try{$db=new PDO('sqlite:/var/www/html/data/owncloud.db');$db->exec(base64_decode('${b64}'));echo"ok";}catch(Exception $e){echo"ERR:".$e->getMessage();}` + await runExec(['php', '-r', php]).catch(() => {}) + + // Touch the real directory too — NC's lazy scanner reads the filesystem mtime + // and would overwrite our DB value if they diverged. + await runExec(['touch', '-m', '-d', `@${mtime}`, `/var/www/html/data/${user}/files/${folderPath}`]).catch(() => {}) +} + +test.beforeAll(async ({ browser }) => { + await tryOcc('user:add --password-from-env --display-name="Christine" christine', { OC_PASS: 'christine' }) + await uploadAvatar(`${AVATAR_DIR}/christine/avatar.png`, 'christine', 'christine') + + // Peer users for sharing — tryOcc is idempotent + await tryOcc('user:add --password-from-env --display-name="Amara Winterbourne" amara_w', { OC_PASS: 'amara_w' }) + await uploadAvatar(`${AVATAR_DIR}/amara_w/avatar.png`, 'amara_w', 'amara_w') + await tryOcc('user:add --password-from-env --display-name="Malik Santiago" malik_s', { OC_PASS: 'malik_s' }) + await uploadAvatar(`${AVATAR_DIR}/Malik_Santiago/avatar.png`, 'malik_s', 'malik_s') + + await mkdavCol('Documents', 'christine', 'christine') + await mkdavCol('Photos', 'christine', 'christine') + await mkdavCol('Projects', 'christine', 'christine') + + // Photos + await uploadFile(`${WALLPAPERS}/forest-green.jpg`, 'Photos/Forest.jpg', 'christine', 'christine', d('2026-03-15')) + await uploadFile(`${WALLPAPERS}/milky-way.jpg`, 'Photos/Milky Way.jpg', 'christine', 'christine', d('2026-02-08')) + await uploadFile(`${WALLPAPERS}/city-night-purple.jpg`, 'Photos/City at night.jpg', 'christine', 'christine', d('2026-01-22')) + await uploadFile(`${WALLPAPERS}/red-desert.jpg`, 'Photos/Red desert.jpg', 'christine', 'christine', d('2026-01-05')) + + // Root-level files — mix of types for a realistic file list + await uploadFile(`${WALLPAPERS}/ocean-golden.jpg`, 'Ocean sunset.jpg', 'christine', 'christine', d('2026-04-10')) + await uploadFile(`${WALLPAPERS}/snowy-mountain.jpg`, 'Snowy mountain.jpg', 'christine', 'christine', d('2025-12-28')) + await uploadFile(`${FIXTURES_PDFS}/Q2 Project Proposal.pdf`, 'Q2 Project Proposal.pdf', 'christine', 'christine', d('2026-04-14')) + await uploadFile( + tmpFile('nc-seed-pitch.md', [ + '# Autumn Gala — Fundraising Pitch', + '', + '## Objective', + 'Raise £40,000 for the community arts centre restoration fund.', + '', + '## Key asks', + '- Headline sponsor: £10,000 (naming rights, table of 10)', + '- Supporting sponsor: £5,000 (logo on materials, 4 tickets)', + '- Friend of the Gala: £1,000 (2 tickets, programme credit)', + '', + '## Timeline', + '- 2 June: venue confirmed', + '- 15 June: sponsor packs out', + '- 1 September: event date', + ].join('\n')), + 'Fundraising Pitch.md', 'christine', 'christine', d('2026-05-02'), + ) + + // Documents + await uploadFile(`${FIXTURES_PDFS}/Team Meeting Notes.pdf`, 'Documents/Team Meeting Notes.pdf', 'christine', 'christine', d('2026-04-28')) + await uploadFile( + tmpFile('nc-seed-agenda.md', [ + '# Q3 Event Planning — Meeting Agenda', + '', + '**Date:** 14 May 2026 **Location:** Video call', + '', + '1. Review Q2 fundraising results', + '2. Confirm venue for autumn gala', + '3. Assign catering and AV responsibilities', + '4. Set sponsor outreach targets', + '5. AOB', + ].join('\n')), + 'Documents/Q3 Meeting Agenda.md', 'christine', 'christine', d('2026-05-14'), + ) + await uploadFile( + tmpFile('nc-seed-budget.csv', [ + 'Category,Budgeted (£),Actual (£),Variance (£)', + 'Venue,5000,4800,200', + 'Catering,3000,3200,-200', + 'Marketing,1500,1200,300', + 'AV Equipment,800,800,0', + 'Miscellaneous,500,320,180', + 'Total,10800,10320,480', + ].join('\n')), + 'Documents/Event Budget.csv', 'christine', 'christine', d('2026-05-10'), + ) + + // Word documents — created via LibreOffice for proper .docx format + await uploadFile( + createDocx('nc-seed-proposal', [ + 'Project Proposal: Autumn Gala 2026', + '', + 'Prepared by: Christine', + 'Date: 1 May 2026', + '', + 'Executive Summary', + 'This proposal outlines the plan for the autumn fundraising gala, targeting', + 'a net raise of £40,000 for the community arts centre restoration fund.', + '', + 'Objectives', + '1. Secure a minimum of 8 sponsors at Supporting level or above', + '2. Sell 180 of 200 available seats', + '3. Run a silent auction with a minimum of 20 lots', + '', + 'Timeline', + '2 June Venue confirmed', + '15 June Sponsor packs distributed', + '1 July Ticket sales open', + '1 September Event date', + ].join('\n')), + 'Documents/Gala Proposal 2026.docx', 'christine', 'christine', d('2026-05-01'), + ) + await uploadFile( + createDocx('nc-seed-agreement', [ + 'Volunteer Agreement', + '', + 'Organisation: Community Arts Centre', + 'Event: Autumn Gala, 1 September 2026', + '', + 'By signing this agreement the volunteer confirms they are available on the', + 'event date and agree to follow all health and safety guidelines.', + '', + 'Roles available: Registration desk, Auction assistant, Front-of-house', + '', + 'Contact: Christine (events@example.org)', + ].join('\n')), + 'Volunteer Agreement.docx', 'christine', 'christine', d('2026-05-18'), + ) + + // Second upload of Q2 Proposal creates a version entry (needed for Versions tab screenshot) + await uploadFile(`${FIXTURES_PDFS}/Q2 Project Proposal.pdf`, 'Q2 Project Proposal.pdf', 'christine', 'christine', d('2026-04-28')) + + // Outgoing shares — Christine → others + // Documents folder shared with Amara and admin (Shared badge; also populates sharing panel) + await ocsRequest('POST', '/ocs/v2.php/apps/files_sharing/api/v1/shares', 'christine', 'christine', { + path: '/Documents', shareType: '0', shareWith: 'admin', + }) + await ocsRequest('POST', '/ocs/v2.php/apps/files_sharing/api/v1/shares', 'christine', 'christine', { + path: '/Documents', shareType: '0', shareWith: 'amara_w', + }) + // Q2 Proposal shared with Malik (shows Shared badge on file) + await ocsRequest('POST', '/ocs/v2.php/apps/files_sharing/api/v1/shares', 'christine', 'christine', { + path: '/Q2 Project Proposal.pdf', shareType: '0', shareWith: 'malik_s', + }) + // Ocean sunset.jpg via public link (chain-link icon) + await ocsRequest('POST', '/ocs/v2.php/apps/files_sharing/api/v1/shares', 'christine', 'christine', { + path: '/Ocean sunset.jpg', shareType: '3', + }) + + // Incoming share — Amara shares a file with Christine (populates Shared with you) + await uploadFile( + tmpFile('nc-seed-venue.md', [ + '# Venue Scouting Notes — Autumn Gala', + '', + '## Riverside Pavilion', + '- Capacity: 200 seated, 280 standing', + '- Rate: £3,800 for Saturday evening', + '- Catering: preferred supplier list, external allowed +15%', + '- Parking: 80 spaces, free after 18:00', + '', + '## City Hall Great Room', + '- Capacity: 150 seated', + '- Rate: £5,200 all-in', + '- Note: booking window closes 31 May', + ].join('\n')), + 'Venue Scouting Notes.md', 'amara_w', 'amara_w', d('2026-05-16'), + ) + await ocsRequest('POST', '/ocs/v2.php/apps/files_sharing/api/v1/shares', 'amara_w', 'amara_w', { + path: '/Venue Scouting Notes.md', shareType: '0', shareWith: 'christine', + }) + + // Remove NC's auto-generated welcome file so it doesn't clutter the screenshots + await fetch(`http://localhost:8093/remote.php/dav/files/christine/welcome.txt`, { + method: 'DELETE', + headers: { Authorization: 'Basic ' + Buffer.from('christine:christine').toString('base64') }, + }) + + // Set Christine's user status so profile screenshots show it + await ocsRequest('PUT', '/ocs/v2.php/apps/user_status/api/v1/user_status/status', 'christine', 'christine', { + statusType: 'online', + }) + await ocsRequest('PUT', '/ocs/v2.php/apps/user_status/api/v1/user_status/message/custom', 'christine', 'christine', { + message: 'Working on Q3 event planning', + statusIcon: '', + }) + + // Login once and cache the session cookies — restored in beforeEach to avoid + // triggering NC brute-force protection with repeated POST /login calls. + const ctx = await browser.newContext() + const pg = await ctx.newPage() + await login(pg.request, user) + authCookies = await ctx.cookies() + await ctx.close() + + // Backdate folder mtimes after login (Talk initialises on first login and would + // overwrite its mtime if we ran earlier). Updates both oc_filecache and the + // filesystem so NC's lazy scanner doesn't revert the values. + // Note: the Talk folder mtime is also intercepted at PROPFIND time in the + // "main view" test because NC's in-process Sabre/DAV cache ignores the DB. + await backdateFolderMtime('Documents', 'christine', d('2026-05-14')) + await backdateFolderMtime('Photos', 'christine', d('2026-03-15')) + await backdateFolderMtime('Projects', 'christine', d('2026-04-01')) + await backdateFolderMtime('Notes', 'christine', d('2026-04-20')) + await backdateFolderMtime('Talk', 'christine', d('2026-05-15')) +}) + +test.beforeEach(async ({ page }) => { + await page.context().addCookies(authCookies) + // NC's in-process Sabre/DAV cache serves today's mtime for the Talk folder + // regardless of what oc_filecache or the filesystem contains — the value is + // cached from first use within the Apache worker's lifetime. Intercept the + // root PROPFIND and rewrite Talk's getlastmodified to the backdated value. + await page.route('**/remote.php/dav/files/christine/', async (route, request) => { + if (request.method() !== 'PROPFIND') { await route.continue(); return } + try { + const response = await route.fetch() + const body = await response.text() + const patched = body.replace( + /([^<]*\/Talk\/<\/d:href>[\s\S]*?)(.*?)(<\/d:getlastmodified>)/, + '$1Thu, 15 May 2026 00:00:00 GMT$3', + ) + await route.fulfill({ response, body: patched, contentType: response.headers()['content-type'] || 'application/xml' }) + } catch (_) { + await route.continue().catch(() => {}) + } + }) +}) + +// ── access_webgui.rst ──────────────────────────────────────────────────────── + +test('Files — main view (users-files)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-content]').waitFor({ state: 'visible' }) + await page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) + await docScreenshot(page, 'user/users-files') +}) + +test('Files — new file/upload menu (files_page-1)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) + await page.locator('[data-cy-upload-picker] button').first().click() + await page.locator('[role="menuitem"]').first().waitFor({ state: 'visible' }) + await docScreenshot(page, 'user/files_page-1') +}) + +test('Files — file row with actions menu (files_page-3)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) + await page.locator('[data-cy-files-list-row]').first().locator('button[aria-label="Actions"]').click() + await page.locator('[data-cy-files-list-row-action]').first().waitFor({ state: 'visible' }) + await docScreenshot(page, 'user/files_page-3') +}) + +test('Files — details sidebar (files_page-4)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) + await page.locator('[data-cy-files-list-row][data-cy-files-list-row-name="Q2 Project Proposal.pdf"]') + .locator('button[aria-label="Actions"]').click({ force: true }) + await page.locator('[data-cy-files-list-row-action="details"]').first().click() + await page.locator('[data-cy-sidebar]').waitFor({ state: 'visible' }) + await docScreenshot(page, 'user/files_page-4') +}) + +test('Files — left navigation panel (files_page-5)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-navigation]').waitFor({ state: 'visible' }) + await docElementScreenshot(page, '[data-cy-files-navigation]', 'user/files_page-5') +}) + +test('Files — breadcrumbs inside a folder (files_page-6)', async ({ page }) => { + await page.goto('/apps/files/files?dir=/Documents') + await page.locator('[data-cy-files-content-breadcrumbs]').waitFor({ state: 'visible' }) + await docElementScreenshot(page, '[data-cy-files-content-breadcrumbs]', 'user/files_page-6') +}) + +test('Files — search / filter (files_page-7)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-navigation]').waitFor({ state: 'visible' }) + await page.locator('.app-navigation-search input, [data-cy-app-navigation-search] input').waitFor({ state: 'visible' }) + await page.locator('.app-navigation-search input, [data-cy-app-navigation-search] input').fill('Document') + await page.waitForTimeout(500) + await docScreenshot(page, 'user/files_page-7') +}) + +test('Files — grid view (files_page-8)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) + // Ensure we start in list view, then switch to grid + const inGrid = await page.locator('.files-list--grid').isVisible() + if (!inGrid) { + await page.locator('.files-list__header-grid-button').click() + await page.locator('.files-list--grid').waitFor({ state: 'attached', timeout: 5000 }) + } + // Wait for thumbnails to render, then blur focus into a safe area (header) + // so button outlines don't appear. Avoid clicking the file area which opens the Viewer. + await page.waitForTimeout(1000) + await page.locator('[data-cy-files-content-breadcrumbs]').click({ force: true }) + await docScreenshot(page, 'user/files_page-8') + // Reset to list view — Escape first in case the breadcrumb click opened anything + await page.keyboard.press('Escape') + await page.locator('.files-list__header-grid-button').click() + await page.locator('.files-list--grid').waitFor({ state: 'detached', timeout: 5000 }).catch(() => {}) +}) + +test('Files — comment in sidebar (file_menu_comments_2)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) + await page.locator('[data-cy-files-list-row]').first().locator('button[aria-label="Actions"]').click({ force: true }) + await page.locator('[data-cy-files-list-row-action="details"]').first().click() + await page.locator('[data-cy-sidebar]').waitFor({ state: 'visible' }) + await page.locator('[role="tab"]', { hasText: 'Activity' }).click() + await page.locator('[role="tabpanel"].app-sidebar__tab--active').waitFor({ state: 'visible' }) + await docScreenshot(page, 'user/file_menu_comments_2') +}) + +test('Files — selecting multiple files (files_page-9)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) + // Select three named user files so the screenshot shows file checkboxes, + // not the system folders that happen to be first in alphabetical order. + for (const name of ['Q2 Project Proposal.pdf', 'Volunteer Agreement.docx', 'Fundraising Pitch.md']) { + await page.locator(`[data-cy-files-list-row][data-cy-files-list-row-name="${name}"]`) + .locator('[data-cy-files-list-row-checkbox]').click() + } + await page.locator('[data-cy-files-list-selection-actions]').waitFor({ state: 'visible' }) + await docScreenshot(page, 'user/files_page-9') +}) + +test('Files — sharing status icons (files_sharing_status)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) + // Wait until all provisioned files appear + await page.waitForFunction(() => document.querySelectorAll('[data-cy-files-list-row]').length > 3, undefined, { timeout: 15000 }) + await page.waitForTimeout(500) + await docScreenshot(page, 'user/files_sharing_status') +}) + +// ── sharing.rst ─────────────────────────────────────────────────────────────── + +test('Files — sharing panel (sharing_internal)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) + await page.locator('[data-cy-files-list-row]').first().locator('button[aria-label="Actions"]').click({ force: true }) + await page.locator('[data-cy-files-list-row-action="details"]').first().click() + await page.locator('[data-cy-sidebar]').waitFor({ state: 'visible' }) + await page.locator('[role="tab"]', { hasText: 'Sharing' }).click() + await page.locator('[role="tabpanel"].app-sidebar__tab--active').waitFor({ state: 'visible' }) + await docScreenshot(page, 'user/sharing_internal') +}) + +test('Files — public link share (sharing_public_file)', async ({ page }) => { + await page.goto('/apps/files/files?dir=/') + await page.locator('[data-cy-files-list]').waitFor({ state: 'visible' }) + + // Close sidebar if NC's router restored it from a previous state + const sidebar = page.locator('[data-cy-sidebar]') + if (await sidebar.isVisible()) { + await page.keyboard.press('Escape') + await sidebar.waitFor({ state: 'hidden' }) + } + + await page.locator('[data-cy-files-list-row]').first().locator('button[aria-label="Actions"]').waitFor({ state: 'visible' }) + await page.locator('[data-cy-files-list-row]').first().locator('button[aria-label="Actions"]').click() + await page.locator('[data-cy-files-list-row-action="details"]').first().click() + await page.locator('[data-cy-sidebar]').waitFor({ state: 'visible' }) + await page.locator('[role="tab"]', { hasText: 'Sharing' }).click() + await page.locator('[role="tabpanel"].app-sidebar__tab--active').waitFor({ state: 'visible' }) + await page.locator('button[aria-label="Create a new share link"]').click() + await page.locator('.sharing-entry.sharing-entry--share').waitFor({ state: 'visible' }) + + // Dismiss toasts + const toastBtns = page.locator('button.toast-close') + for (let i = 0; i < await toastBtns.count(); i++) { + await toastBtns.nth(i).click({ force: true }).catch(() => {}) + } + await page.locator('.toastify').waitFor({ state: 'detached' }).catch(() => {}) + + await docScreenshot(page, 'user/sharing_public_file') +}) + +// ── quota.rst ───────────────────────────────────────────────────────────────── + +test('Files — quota display (quota1)', async ({ page }) => { + await page.goto('/apps/files') + await page.locator('[data-cy-files-navigation-settings-quota]').waitFor({ state: 'visible' }) + await docElementScreenshot(page, '[data-cy-files-navigation-settings-quota]', 'user/quota1') +}) diff --git a/playwright/e2e/user/talk/conversations.spec.ts b/playwright/e2e/user/talk/conversations.spec.ts new file mode 100644 index 00000000000..242b2593737 --- /dev/null +++ b/playwright/e2e/user/talk/conversations.spec.ts @@ -0,0 +1,692 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { test } from '@playwright/test' +import { User } from '@nextcloud/e2e-test-server' +import { + docScreenshot, + docElementScreenshot, + ocsRequest, + seedChatMessages, +} from '../../../helpers' +import { Page } from '@playwright/test' +import * as path from 'path' +import * as os from 'os' +import * as fs from 'fs/promises' + +test.describe.configure({ mode: 'serial' }) + +const christine = new User('christine', 'christine') + + +// Token for the "Event planning" group conversation — populated in beforeAll +let groupToken = '' + +// ── Talk OCS helpers ────────────────────────────────────────────────────────── + +async function talkApi(method: string, talkPath: string, user: User, body?: Record) { + return ocsRequest(method, `/ocs/v2.php/apps/spreed/api${talkPath}`, user.userId, user.password, body) +} + +async function createGroup(name: string, as: User): Promise { + const res = await talkApi('POST', '/v4/room', as, { roomType: '2', roomName: name }) + const data = await res.json() + return data.ocs.data.token as string +} + +async function addParticipant(token: string, uid: string, as: User): Promise { + await talkApi('POST', `/v4/room/${token}/participants`, as, { newParticipant: uid, source: 'users' }) +} + + +async function findOrCreateGroup(): Promise { + const res = await talkApi('GET', '/v4/room', christine) + const data = await res.json() + const rooms: Array<{ token: string; displayName: string; isArchived?: boolean }> = data?.ocs?.data ?? [] + const existing = rooms.find((r) => r.displayName === 'Event planning') + + if (existing?.isArchived) { + await talkApi('DELETE', `/v4/room/${existing.token}/archive`, christine) + groupToken = existing.token + return groupToken + } + + if (existing) { + groupToken = existing.token + return groupToken + } + + const token = await createGroup('Event planning', christine) + groupToken = token + await addParticipant(token, 'amara_w', christine) + await seedChatMessages(token, [ + { text: "Hi team! I've set up this conversation for coordinating the Q3 fundraising event.", user: 'christine', password: 'christine' }, + { text: 'Great, thanks for setting this up! I have a few updates to share.', user: 'amara_w', password: 'amara_w' }, + { text: "Looking forward to hearing them. Let's get started!", user: 'christine', password: 'christine' }, + ]) + return groupToken +} + +async function getOrCreateGroupToken(): Promise { + if (groupToken) { + const res = await talkApi('GET', `/v4/room/${groupToken}`, christine) + const data = await res.json() + const room = data?.ocs?.data + if (res.status === 200 && !room?.isArchived) return groupToken + if (res.status === 200 && room?.isArchived) { + await talkApi('DELETE', `/v4/room/${groupToken}/archive`, christine) + return groupToken + } + groupToken = '' + } + return findOrCreateGroup() +} + +// ── Talk UI helpers ──────────────────────────────────────────────────────────── + +async function openConversation(page: Page, displayName: string): Promise { + await page.locator('.conversation .text', { hasText: displayName }).waitFor({ state: 'visible', timeout: 15000 }) + await page.locator('.conversation .text', { hasText: displayName }).click() + await page.locator('.chatView').waitFor({ state: 'visible', timeout: 10000 }) +} + +async function openGroupConversation(page: Page, token: string): Promise { + await page.goto(`/call/${token}`) + await page.locator('.chatView').waitFor({ state: 'visible', timeout: 15000 }) +} + +async function openSidebar(page: Page): Promise { + const toggleBtn = page.locator('.app-sidebar__toggle') + if (await toggleBtn.isVisible()) { + await toggleBtn.click() + } + await page.locator('.app-sidebar').waitFor({ state: 'visible', timeout: 5000 }) +} + +async function openConversationActions(page: Page): Promise { + await page.locator('button[aria-label="Conversation actions"]').first().waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('button[aria-label="Conversation actions"]').first().click() + await page.locator('[role="menu"]').waitFor({ state: 'visible', timeout: 5000 }) +} + +async function clearTalkFilter(page: Page): Promise { + // localStorage is inaccessible on about:blank; silently skip — no filter to clear + await page.evaluate(() => { try { localStorage.removeItem('nextcloud_per_dGFsaw==_filterEnabled') } catch {} }) +} + +// ── Provisioning ────────────────────────────────────────────────────────────── + +test.beforeAll(async () => { + // All seeding is done in global-setup. Fetch the group token so per-test + // helpers that need it (e.g. openGroupConversation) have it available. + await findOrCreateGroup() +}) + +// ── Screenshots ─────────────────────────────────────────────────────────────── + +test('Schedule a meeting', async ({ page }) => { + // Open the Event planning conversation first to establish the chat context. + const token = await getOrCreateGroupToken() + await page.goto('/apps/spreed') + await page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 15000 }) + await openConversation(page, 'Event planning') + + // Navigate to the Calendar app to create the meeting event. + await page.goto('/apps/calendar') + await page.locator('.fc.fc-media-screen').waitFor({ state: 'visible', timeout: 15000 }) + await page.waitForTimeout(500) + + // Click the "New event" button to open the event editor. + const newEventBtn = page.locator('button[aria-label="Create new event"], button[aria-label="New event"], button:has-text("Create new event"), button:has-text("New event")').first() + await newEventBtn.waitFor({ state: 'visible', timeout: 8000 }) + await newEventBtn.click() + + // Wait for the event creation dialog to open — Calendar uses a custom popover + // whose backdrop is a element while the form lives in a sibling element. + // Anchor on the title input which is reliably present when the form is ready. + const titleInput = page.getByPlaceholder('Title') + await titleInput.waitFor({ state: 'visible', timeout: 8000 }) + await page.waitForTimeout(300) + + // Fill in the event title. + await titleInput.click() + await titleInput.fill('Event planning catchup') + + // Add a Talk call via the calendar integration. + // Clicking "Add Talk conversation" opens a "Select a Talk Room" room picker. + const talkBtn = page.getByRole('button', { name: 'Add Talk conversation' }) + if (await talkBtn.isVisible({ timeout: 3000 }).catch(() => false)) { + await talkBtn.click() + const roomPicker = page.getByRole('dialog', { name: 'Select a Talk Room' }) + await roomPicker.waitFor({ state: 'visible', timeout: 5000 }).catch(() => {}) + // Screenshot shows the room picker with Talk conversations listed. + await docScreenshot(page, 'user/talk/schedule-meeting') + // Dismiss the picker. Escape propagates to the parent event editor and + // triggers a "Discard changes?" dialog — click Cancel to keep the form. + await page.keyboard.press('Escape') + const discardDialog = page.getByRole('dialog', { name: 'Discard changes?' }) + if (await discardDialog.isVisible({ timeout: 2000 }).catch(() => false)) { + await discardDialog.getByRole('button', { name: 'Cancel' }).click() + await discardDialog.waitFor({ state: 'hidden', timeout: 3000 }).catch(() => {}) + } + await roomPicker.waitFor({ state: 'hidden', timeout: 3000 }).catch(() => {}) + await page.waitForTimeout(300) + } else { + await docScreenshot(page, 'user/talk/schedule-meeting') + } + + // Save the event. + const saveBtn = page.getByRole('button', { name: 'Save' }) + if (await saveBtn.isVisible({ timeout: 3000 }).catch(() => false)) { + await saveBtn.click() + await page.waitForTimeout(1000) + } +}) + +test('Talk dashboard', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await Promise.race([ + page.locator('.dashboard__title, h2:has-text("Hello"), .talk-dashboard').waitFor({ state: 'visible', timeout: 10000 }), + page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 10000 }), + ]).catch(() => {}) + await page.locator('.icon-loading').waitFor({ state: 'hidden', timeout: 10000 }).catch(() => {}) + await page.waitForTimeout(1500) + await docScreenshot(page, 'user/talk/talk-dashboard') +}) + +test('Note to self', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await page.locator('.conversation .text', { hasText: 'Note to self' }).waitFor({ state: 'visible', timeout: 15000 }) + await page.locator('.conversation .text', { hasText: 'Note to self' }).click() + await page.locator('.chatView').waitFor({ state: 'visible', timeout: 10000 }) + + const hasTaskList = await page.locator('.chatView').getByText(/Define Project Scope/i).isVisible().catch(() => false) + if (!hasTaskList) { + // Fallback: seed via UI if API seeding didn't land. Locator discovered via page snapshot. + const inputArea = page.getByRole('region', { name: 'Post message' }).getByRole('textbox') + await inputArea.waitFor({ state: 'visible', timeout: 5000 }) + await inputArea.click() + const lines = [ + '- [x] Define Project Scope and Objectives', + '- [x] Develop a Project Plan', + '- [ ] Coordinate Team Activities', + '- [ ] Review and finalize budget', + '- [ ] Schedule kickoff meeting', + ] + for (let i = 0; i < lines.length; i++) { + await page.keyboard.type(lines[i]) + if (i < lines.length - 1) await page.keyboard.press('Shift+Enter') + } + await page.keyboard.press('Enter') + await page.locator('.chatView').getByText(/Define Project Scope/i).waitFor({ state: 'visible', timeout: 10000 }) + } + await page.waitForTimeout(500) + // Close sidebar if open + const sidebar = page.locator('.app-sidebar') + if (await sidebar.isVisible()) { + await sidebar.locator('button[aria-label="Close sidebar"], button[aria-label="Close"]').filter({ hasNotText: '' }).first().click() + await sidebar.waitFor({ state: 'hidden' }) + } + await docScreenshot(page, 'user/talk/note-to-self') +}) + +test('1:1 conversation with right sidebar', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await openConversation(page, 'Amara Winterbourne') + await openSidebar(page) + await page.locator('.app-sidebar', { hasText: 'Event Coordinator' }).waitFor({ state: 'visible', timeout: 8000 }) + const sidebarEl = page.locator('.app-sidebar') + const sidebarBox = await sidebarEl.boundingBox() + const dest = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs', 'user', 'talk', 'one-to-one-right-sidebar.png') + await fs.mkdir(path.dirname(dest), { recursive: true }) + if (sidebarBox) { + await page.screenshot({ path: dest, clip: { x: sidebarBox.x, y: sidebarBox.y, width: sidebarBox.width, height: Math.min(380, sidebarBox.height) } }) + } else { + await sidebarEl.screenshot({ path: dest }) + } +}) + +test('1:1 extend to group', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await openConversation(page, 'Amara Winterbourne') + await page.locator('button[aria-label="Start a group conversation"]').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('button[aria-label="Start a group conversation"]').click() + await page.locator('.start-group__content, [role="dialog"]').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('.start-group__content input, [role="dialog"] input[type="text"]').first().fill('Lila') + await page.locator('[data-nav-id="users_lila_h"]').waitFor({ state: 'visible', timeout: 5000 }).catch(() => {}) + await docScreenshot(page, 'user/talk/one-to-one-extend') +}) + +test('Create new conversation button', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 15000 }) + await page.locator('.new-conversation .actions .action-item__menutoggle').first().click() + await page.locator('[role="menuitem"]', { hasText: 'Create a new conversation' }).waitFor({ state: 'visible', timeout: 5000 }) + await docScreenshot(page, 'user/talk/create-new-conversation', { clip: { x: 0, y: 0, width: 500, height: 350 } }) +}) + +test('Creating open conversation (step 1: name + settings)', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await page.locator('.new-conversation .actions .action-item__menutoggle').first().click() + await page.locator('[role="menuitem"]', { hasText: 'Create a new conversation' }).click() + await page.locator('.new-group-conversation').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('.new-group-conversation input[type="text"]').first().fill('Product team') + + // Set emoji on the conversation avatar if the picker is available in this Talk version. + // All clicks use short explicit timeouts — without them, Playwright inherits the full + // test timeout (60 s) and blocks the whole test when an element isn't present. + const emojiBtn = page.locator('.new-group-conversation button[aria-label*="moji"], .new-group-conversation .emoji-picker-trigger').first() + if (await emojiBtn.isVisible({ timeout: 2000 }).catch(() => false)) { + await emojiBtn.click({ timeout: 3000 }).catch(() => {}) + const emojiInput = page.locator('.emoji-mart-search input, input[placeholder*="Search emoji"]').first() + if (await emojiInput.isVisible({ timeout: 2000 }).catch(() => false)) { + // Use pressSequentially so Vue reactivity fires on each keystroke. + // fill() sets the value in one shot and can bypass reactive watchers. + await emojiInput.click({ timeout: 2000 }).catch(() => {}) + await emojiInput.pressSequentially('laptop', { delay: 80 }) + // "Frequently used" disappearing means search results have replaced it + await page.locator('.emoji-mart-category-label').filter({ hasText: /frequently used/i }) + .waitFor({ state: 'hidden', timeout: 3000 }).catch(() => {}) + await page.locator('.emoji-mart-emoji').first().click({ timeout: 3000 }).catch(() => {}) + } + } + + // Fill in description if the field is present + const descriptionField = page.locator('.new-group-conversation textarea, .new-group-conversation input[placeholder*="escription"]').first() + if (await descriptionField.isVisible({ timeout: 2000 }).catch(() => false)) { + await descriptionField.fill('Discuss product priorities, roadmap, and cross-team updates.') + } + + await docScreenshot(page, 'user/talk/creating-open-conversation') +}) + +test('Add participants (step 2)', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await page.locator('.new-conversation .actions .action-item__menutoggle').first().click() + await page.locator('[role="menuitem"]', { hasText: 'Create a new conversation' }).click() + await page.locator('.new-group-conversation').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('.new-group-conversation input[type="text"]').first().fill('Product team') + await page.locator('button', { hasText: /add participants/i }).click() + await page.locator('.new-group-conversation').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('.new-group-conversation input[type="text"]').last().fill('l') + await page.locator('[data-nav-id="users_lila_h"]').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('[data-nav-id="users_lila_h"]').click() + await docScreenshot(page, 'user/talk/add-participants') +}) + +test('New room (freshly created conversation)', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await page.locator('.new-conversation .actions .action-item__menutoggle').first().click() + await page.locator('[role="menuitem"]', { hasText: 'Create a new conversation' }).click() + await page.locator('.new-group-conversation').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('.new-group-conversation input[type="text"]').first().fill('Product team') + await page.locator('button', { hasText: /add participants/i }).click() + const participantsInput = page.locator('.new-group-conversation input[type="text"]').last() + await participantsInput.waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('[data-nav-id="users_amara_w"]').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('[data-nav-id="users_amara_w"]').click() + await participantsInput.fill('lila') + await page.locator('[data-nav-id="users_lila_h"]').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('[data-nav-id="users_lila_h"]').click() + await participantsInput.fill('malik') + await page.locator('[data-nav-id="users_malik_s"]').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('[data-nav-id="users_malik_s"]').click() + await page.locator('button', { hasText: /create conversation/i }).click() + await page.locator('.chatView').waitFor({ state: 'visible', timeout: 15000 }) + // Extract token from URL and seed messages + const newRoomUrl = page.url() + const newRoomToken = newRoomUrl.match(/\/call\/([a-z0-9]+)/i)?.[1] + if (newRoomToken) { + // Set the laptop emoji to match what the creating-open-conversation screenshot shows + await talkApi('POST', `/v1/room/${newRoomToken}/avatar/emoji`, christine, { emoji: '💻', color: '0082c9' }).catch(() => {}) + await seedChatMessages(newRoomToken, [ + { text: "Hey team! Welcome to the Product Team chat 👋", user: 'christine', password: 'christine' }, + { text: "Thanks for setting this up!", user: 'amara_w', password: 'amara_w' }, + { text: "Excited to collaborate here — what's our first agenda item?", user: 'lila_h', password: 'lila_h' }, + { text: "Let's start with the Q3 roadmap review. I'll share the doc shortly.", user: 'christine', password: 'christine' }, + { text: "I have a few feature requests from the last sprint to add to that.", user: 'malik_s', password: 'malik_s' }, + { text: "Great, let's go through them all in tomorrow's sync.", user: 'amara_w', password: 'amara_w' }, + ]) + await page.reload() + await page.locator('.chatView').waitFor({ state: 'visible', timeout: 15000 }) + await page.locator('.chatView').getByText(/Hey team! Welcome to the Product Team chat/i).waitFor({ state: 'visible', timeout: 10000 }).catch(() => {}) + } + // Avoid waiting generically for .icon-loading — shared-items-tab spinner may persist + await page.locator('.icon-loading:not(.shared-items-tab__loading)').waitFor({ state: 'hidden', timeout: 10000 }).catch(() => {}) + await docScreenshot(page, 'user/talk/new-room') +}) + +test('Filters menu', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 15000 }) + await page.locator('.new-conversation .filters .action-item__menutoggle').first().click() + await page.locator('[role="menu"]').waitFor({ state: 'visible', timeout: 5000 }) + await docScreenshot(page, 'user/talk/filters-menu', { clip: { x: 0, y: 0, width: 500, height: 350 } }) +}) + +test('Clear filter', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 15000 }) + await page.locator('.new-conversation .filters .action-item__menutoggle').first().click() + await page.locator('[role="menu"]').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('[role="menuitemcheckbox"]', { hasText: /unread messages/i }).click() + await page.locator('.new-conversation .filters .action-item__menutoggle').first().click() + await page.locator('[role="menu"]').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('[role="menuitem"]', { hasText: /clear filters/i }).waitFor({ state: 'visible', timeout: 5000 }) + await docScreenshot(page, 'user/talk/clear-filter', { clip: { x: 0, y: 0, width: 500, height: 350 } }) + await page.locator('[role="menuitem"]', { hasText: /clear filters/i }).click() +}) + +test('Group public settings', async ({ page }) => { + const token = await getOrCreateGroupToken() + await openGroupConversation(page, token) + await openConversationActions(page) + await page.locator('[role="menuitem"]', { hasText: /conversation settings/i }).click() + const container = page.locator('#conversation-settings-container') + await container.waitFor({ state: 'visible', timeout: 10000 }) + // Open/guest-access toggles live in the Moderation section + await container.locator('.navigation-list__link', { hasText: /moderation/i }).click() + // #settings-section_conversation-settings is the Moderation panel element + const moderationSection = page.locator('#settings-section_conversation-settings') + await moderationSection.waitFor({ state: 'visible', timeout: 10000 }) + await moderationSection.scrollIntoViewIfNeeded() + await page.waitForTimeout(500) + // Clip to just the top of the section (open/guest-access toggles) with 20px padding + const box = await moderationSection.boundingBox() + const dest = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs', 'user', 'talk', 'group-public-settings.png') + await fs.mkdir(path.dirname(dest), { recursive: true }) + if (box) { + await page.screenshot({ path: dest, clip: { x: box.x - 20, y: box.y - 20, width: box.width + 40, height: Math.min(box.height, 280) + 40 } }) + } else { + await moderationSection.screenshot({ path: dest }) + } + await container.locator('button[aria-label="Close"]').click() +}) + +test('Participant menu (... on participant)', async ({ page }) => { + const token = await getOrCreateGroupToken() + await openGroupConversation(page, token) + await openSidebar(page) + await page.locator('#tab-button-participants').waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('#tab-button-participants').click() + await page.locator('#tab-participants').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('.participant', { hasText: 'Amara Winterbourne' }).waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('.participant', { hasText: 'Amara Winterbourne' }).locator('button[aria-label*="Settings for participant"]').first().click() + await page.locator('[role="menu"]').waitFor({ state: 'visible', timeout: 5000 }) + const menuEl = page.locator('[role="menu"]').first() + const menuBox = await menuEl.boundingBox() + const dest = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs', 'user', 'talk', 'participant-menu.png') + await fs.mkdir(path.dirname(dest), { recursive: true }) + if (menuBox) { + await page.screenshot({ path: dest, clip: { x: menuBox.x - 16, y: menuBox.y - 16, width: menuBox.width + 32, height: menuBox.height + 32 } }) + } else { + await menuEl.screenshot({ path: dest }) + } +}) + +test('Open conversation settings menu', async ({ page }) => { + const token = await getOrCreateGroupToken() + await openGroupConversation(page, token) + await openConversationActions(page) + const menuEl = page.locator('[role="menu"]').first() + const menuBox = await menuEl.boundingBox() + const dest = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs', 'user', 'talk', 'open-settings.png') + await fs.mkdir(path.dirname(dest), { recursive: true }) + if (menuBox) { + await page.screenshot({ path: dest, clip: { x: menuBox.x - 16, y: menuBox.y - 16, width: menuBox.width + 32, height: menuBox.height + 32 } }) + } else { + await menuEl.screenshot({ path: dest }) + } +}) + +test('Conversation settings dialog', async ({ page }) => { + const token = await getOrCreateGroupToken() + await openGroupConversation(page, token) + await openConversationActions(page) + await page.locator('[role="menuitem"]', { hasText: /conversation settings/i }).click() + await page.locator('#conversation-settings-container').waitFor({ state: 'visible', timeout: 10000 }) + await docElementScreenshot(page, '#conversation-settings-container', 'user/talk/conversation-settings-dialog') +}) + +test('Message expiration setting', async ({ page }) => { + const token = await getOrCreateGroupToken() + await openGroupConversation(page, token) + await openConversationActions(page) + await page.locator('[role="menuitem"]', { hasText: /conversation settings/i }).click() + await page.locator('#conversation-settings-container').waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('.navigation-list__link', { hasText: /moderation/i }).click() + const moderationSection = page.locator('#settings-section_conversation-settings') + await moderationSection.waitFor({ state: 'visible', timeout: 10000 }) + // Message expiration only appears when backgroundjobs_mode=cron (configured in global-setup) + const expirationLabel = moderationSection.getByText(/message expiration/i).first() + await expirationLabel.waitFor({ state: 'visible', timeout: 10000 }) + await expirationLabel.scrollIntoViewIfNeeded() + await page.waitForTimeout(500) + const dest = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs', 'user', 'talk', 'messages-expiration.png') + await fs.mkdir(path.dirname(dest), { recursive: true }) + const sectionBox = await moderationSection.boundingBox() + const labelBox = await expirationLabel.boundingBox() + if (sectionBox && labelBox) { + await page.screenshot({ path: dest, clip: { + x: sectionBox.x - 20, y: labelBox.y - 32, + width: sectionBox.width + 40, height: Math.min(160, sectionBox.y + sectionBox.height - labelBox.y) + 40, + } }) + } else { + await moderationSection.screenshot({ path: dest }) + } +}) + +test('Ban participant', async ({ page }) => { + const token = await getOrCreateGroupToken() + await openGroupConversation(page, token) + await openSidebar(page) + await page.locator('#tab-button-participants').waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('#tab-button-participants').click() + await page.locator('.participant', { hasText: 'Amara Winterbourne' }).waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('.participant', { hasText: 'Amara Winterbourne' }).locator('button[aria-label*="Settings for participant"]').first().click() + await page.locator('[role="menuitem"]', { hasText: /remove participant/i }).waitFor({ state: 'visible', timeout: 5000 }) + const menuEl = page.locator('[role="menu"]').first() + const menuBox = await menuEl.boundingBox() + const dest = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs', 'user', 'talk', 'ban-participant.png') + await fs.mkdir(path.dirname(dest), { recursive: true }) + if (menuBox) { + await page.screenshot({ path: dest, clip: { x: menuBox.x - 16, y: menuBox.y - 16, width: menuBox.width + 32, height: menuBox.height + 32 } }) + } else { + await menuEl.screenshot({ path: dest }) + } +}) + +test('Ban participant dialog', async ({ page }) => { + const token = await getOrCreateGroupToken() + + // Ensure Amara is not already banned + const banRes = await talkApi('GET', `/v1/ban/${token}`, christine) + const banData = await banRes.json() + const bans: Array<{ id: number; actorId: string }> = banData?.ocs?.data ?? [] + const amaraBan = bans.find((b) => b.actorId === 'amara_w') + if (amaraBan) { + await talkApi('DELETE', `/v1/ban/${token}/${amaraBan.id}`, christine) + await addParticipant(token, 'amara_w', christine) + } + + await openGroupConversation(page, token) + await openSidebar(page) + await page.locator('#tab-button-participants').waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('#tab-button-participants').click() + await page.locator('.participant', { hasText: 'Amara Winterbourne' }).waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('.participant', { hasText: 'Amara Winterbourne' }).locator('button[aria-label*="Settings for participant"]').first().click() + await page.locator('[role="menuitem"]', { hasText: /remove participant/i }).waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('[role="menuitem"]', { hasText: /remove participant/i }).click() + await page.locator('.dialog').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('.checkbox-radio-switch input[type="checkbox"]').check({ force: true }) + await docScreenshot(page, 'user/talk/ban-participant-dialog') + // Confirm the ban so the ban list has content for the next test + await page.locator('.dialog button', { hasText: /remove/i }).click() + await page.locator('.dialog').waitFor({ state: 'detached', timeout: 5000 }) +}) + +test('Ban participant list', async ({ page }) => { + const token = await getOrCreateGroupToken() + + // Pre-ban lila_h and malik_s to populate the ban list + await talkApi('POST', `/v1/ban/${token}`, christine, { actorType: 'users', actorId: 'lila_h', internalNote: 'Documentation screenshot' }).catch(() => {}) + await talkApi('POST', `/v1/ban/${token}`, christine, { actorType: 'users', actorId: 'malik_s', internalNote: 'Documentation screenshot' }).catch(() => {}) + + await openGroupConversation(page, token) + await openConversationActions(page) + await page.locator('[role="menuitem"]', { hasText: /conversation settings/i }).click() + await page.locator('#conversation-settings-container').waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('.navigation-list__link', { hasText: /moderation/i }).click() + await page.locator('#settings-section_conversation-settings').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('#settings-section_conversation-settings').scrollIntoViewIfNeeded() + await page.locator('button:has-text("Manage bans")').waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('button:has-text("Manage bans")').click() + // "Manage bans" navigates within the settings container to the banned-users view. + // Wait for the Moderation section to animate out before screenshotting. + const banDialog = page.getByRole('dialog', { name: /banned users/i }) + await banDialog.waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('#settings-section_conversation-settings').waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {}) + await page.waitForTimeout(400) + const dest = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs', 'user', 'talk', 'ban-participant-list.png') + await fs.mkdir(path.dirname(dest), { recursive: true }) + await banDialog.screenshot({ path: dest }) + // Close the ban dialog, then the settings + await banDialog.getByRole('button', { name: 'Close' }).click() + await banDialog.waitFor({ state: 'hidden', timeout: 5000 }).catch(() => {}) + await page.locator('#conversation-settings-container').locator('button[aria-label="Close"]').first().click() + + // Clean up: fetch all bans, delete all, re-add amara_w + lila_h + malik_s + const banRes2 = await talkApi('GET', `/v1/ban/${token}`, christine) + const banData2 = await banRes2.json() + const bans2: Array<{ id: number; actorId: string }> = banData2?.ocs?.data ?? [] + for (const ban of bans2) { + await talkApi('DELETE', `/v1/ban/${token}/${ban.id}`, christine).catch(() => {}) + } + await addParticipant(token, 'amara_w', christine).catch(() => {}) + await addParticipant(token, 'lila_h', christine).catch(() => {}) + await addParticipant(token, 'malik_s', christine).catch(() => {}) +}) + +test('Conversation notifications setting', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 15000 }) + await page.waitForFunction(() => document.querySelectorAll('.conversation').length >= 1, undefined, { timeout: 15000 }) + const conv = page.locator('.conversation[title="Amara Winterbourne"]') + await conv.waitFor({ state: 'visible', timeout: 15000 }) + await conv.hover() + await conv.locator('button[aria-label="Conversation actions"]').first().click({ force: true }) + await page.locator('[role="menu"]').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('[role="menuitem"]', { hasText: /notification/i }).click() + await page.locator('[role="menu"]').waitFor({ state: 'visible', timeout: 5000 }) + const subMenuEl = page.locator('[role="menu"]').last() + const subMenuBox = await subMenuEl.boundingBox() + const dest = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs', 'user', 'talk', 'conversation-notifications.png') + await fs.mkdir(path.dirname(dest), { recursive: true }) + if (subMenuBox) { + await page.screenshot({ path: dest, clip: { x: subMenuBox.x - 16, y: subMenuBox.y - 16, width: subMenuBox.width + 32, height: subMenuBox.height + 32 } }) + } else { + await subMenuEl.screenshot({ path: dest }) + } +}) + +test('Privacy settings (Talk personal settings)', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 15000 }) + await page.locator('button', { hasText: 'App settings' }).waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('button', { hasText: 'App settings' }).click() + await page.locator('.modal-container').waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('.navigation-list__link', { hasText: /^Privacy$/ }).click() + await page.locator('#settings-section_privacy').waitFor({ state: 'visible', timeout: 5000 }) + await page.locator('#settings-section_privacy').scrollIntoViewIfNeeded() + await docScreenshot(page, 'user/talk/privacy-settings') + await page.locator('.modal-container button[aria-label="Close"]').first().click() +}) + +test('Archived conversations button', async ({ page }) => { + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 15000 }) + const token = await getOrCreateGroupToken() + // Use @all so the archived conversation shows an unread-mention badge on the + // "Archived conversations" button. If the "Unread mentions" navigation button + // appears above it, clip the screenshot to start just below that button so it + // is excluded. The dot on the archive button is preserved. + await seedChatMessages(token, [ + { text: "@all Don't forget the catering walkthrough is Friday at 10am!", user: 'amara_w', password: 'amara_w' }, + ]) + await talkApi('POST', `/v4/room/${token}/archive`, christine) + await page.reload() + await page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 15000 }) + const archivedBtn = page.locator('button', { hasText: 'Archived conversations' }) + await archivedBtn.waitFor({ state: 'visible', timeout: 10000 }) + const listEl = page.locator('[aria-label="Conversation list"]') + const listBox = await listEl.boundingBox() + const btnBox = await archivedBtn.boundingBox() + const dest = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs', 'user', 'talk', 'archived-conversations-button.png') + await fs.mkdir(path.dirname(dest), { recursive: true }) + if (listBox && btnBox) { + // If the "Unread mentions" navigation button is present, start the clip just + // below it to exclude it. Otherwise fall back to ~80px above the archive button. + // Use isVisible() (instant, no timeout) before boundingBox() — boundingBox() + // waits for the element with the full action timeout if it doesn't exist. + const mentionsBtn = page.locator('button', { hasText: /unread mentions/i }) + const mentionsBox = (await mentionsBtn.isVisible()) ? await mentionsBtn.boundingBox() : null + const clipTop = mentionsBox + ? mentionsBox.y + mentionsBox.height + 4 + : btnBox.y - 80 + await page.screenshot({ path: dest, clip: { x: listBox.x, y: clipTop, width: listBox.width, height: btnBox.y + btnBox.height - clipTop + 8 } }) + } else { + await archivedBtn.screenshot({ path: dest }) + } +}) + +test('Archived conversations list', async ({ page }) => { + // Relies on "Archived conversations button" test having archived "Event planning". + // Archive two more rooms so the list looks populated, then unarchive all at the end. + await clearTalkFilter(page) + await page.goto('/apps/spreed') + await page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 15000 }) + + const allRoomsRes = await talkApi('GET', '/v4/room', christine) + const allRoomsData = await allRoomsRes.json() + const rooms: Array<{ token: string; displayName: string }> = allRoomsData?.ocs?.data ?? [] + const designRoom = rooms.find(r => r.displayName === 'Design Team') + const volunteerRoom = rooms.find(r => r.displayName === 'Volunteer Coordination') + if (designRoom) await talkApi('POST', `/v4/room/${designRoom.token}/archive`, christine) + if (volunteerRoom) await talkApi('POST', `/v4/room/${volunteerRoom.token}/archive`, christine) + + await page.reload() + await page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 15000 }) + await page.locator('button', { hasText: 'Archived conversations' }).waitFor({ state: 'visible', timeout: 10000 }) + await page.locator('button', { hasText: 'Archived conversations' }).click() + await page.locator('.conversation[title="Event planning"]').first().waitFor({ state: 'visible', timeout: 10000 }) + + // Crop to the upper half of the conversation list — the list is long and the + // bottom half is empty space; three rows give enough context. + const listEl = page.locator('[aria-label="Conversation list"]') + const listBox = await listEl.boundingBox() + const dest = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs', 'user', 'talk', 'archived-conversations-list.png') + await fs.mkdir(path.dirname(dest), { recursive: true }) + if (listBox) { + await page.screenshot({ path: dest, clip: { x: listBox.x, y: listBox.y, width: listBox.width, height: Math.round(listBox.height / 2) } }) + } else { + await docElementScreenshot(page, '[aria-label="Conversation list"]', 'user/talk/archived-conversations-list') + } + + // Unarchive all for clean subsequent runs + if (groupToken) await talkApi('DELETE', `/v4/room/${groupToken}/archive`, christine) + if (designRoom) await talkApi('DELETE', `/v4/room/${designRoom.token}/archive`, christine) + if (volunteerRoom) await talkApi('DELETE', `/v4/room/${volunteerRoom.token}/archive`, christine) +}) diff --git a/playwright/e2e/user/webinterface.spec.ts b/playwright/e2e/user/webinterface.spec.ts new file mode 100644 index 00000000000..95c231d601c --- /dev/null +++ b/playwright/e2e/user/webinterface.spec.ts @@ -0,0 +1,96 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { test, Cookie } from '@playwright/test' +import { User } from '@nextcloud/e2e-test-server' +import { login } from '@nextcloud/e2e-test-server/playwright' +import { docScreenshot, docElementScreenshot, occ, tryOcc, uploadAvatar, ocsRequest } from '../../helpers' + +const user = new User('christine', 'christine') +const AVATAR_DIR = '/home/anna/Downloads/tp/avatar' + +let authCookies: Cookie[] = [] + +test.beforeAll(async ({ browser }) => { + await tryOcc('user:add --password-from-env --display-name="Christine" christine', { OC_PASS: 'christine' }) + await uploadAvatar(`${AVATAR_DIR}/christine/avatar.png`, 'christine', 'christine') + await tryOcc('user:setting christine dashboard layout files-favorites,calendar,deck,notes') + await tryOcc('user:setting christine dashboard firstRun 0') + + // Seed Notes so the Notes dashboard widget shows content + const notesBase = 'http://localhost:8093' + const auth = 'Basic ' + Buffer.from('christine:christine').toString('base64') + const existingNotes = await fetch(`${notesBase}/apps/notes/api/v1/notes`, { + headers: { Authorization: auth, Accept: 'application/json' }, + }).then(r => r.json()).catch(() => []) + if (!Array.isArray(existingNotes) || existingNotes.length === 0) { + for (const [title, content] of [ + ['Autumn Gala ideas', '- Jazz quartet for the reception\n- Photobooth with charity frame\n- Silent auction: local artwork\n- Ask Riverside if they can do late bar'], + ['Sponsor call — follow-up', 'Spoke to Hartley & Co. on 12 May.\nThey can commit £5k at Supporting level.\nSend contract by end of week.'], + ['Q3 action items', '1. Confirm venue by 2 June\n2. Send sponsor packs by 15 June\n3. Book catering walkthrough\n4. Brief comms team on social plan'], + ]) { + await fetch(`${notesBase}/apps/notes/api/v1/notes`, { + method: 'POST', + headers: { Authorization: auth, 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, content }), + }).catch(() => {}) + } + } + + const ctx = await browser.newContext() + const pg = await ctx.newPage() + await login(pg.request, user) + authCookies = await ctx.cookies() + await ctx.close() +}) + +test.beforeEach(async ({ page }) => { + await page.context().addCookies(authCookies) +}) + +test('Login page', async ({ page }) => { + await page.context().clearCookies() + await page.goto('/') + await page.locator('form[name="login"]').waitFor({ state: 'visible' }) + await docScreenshot(page, 'user/login_page') +}) + +test('Dashboard', async ({ page }) => { + await page.goto('/apps/dashboard') + await page.locator('.panel--header, .dashboard-widget-content').first().waitFor({ state: 'visible', timeout: 15000 }) + await page.locator('.icon-loading').waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {}) + await docScreenshot(page, 'user/webinterface_dashboard') +}) + +test('Navigation bar', async ({ page }) => { + await page.goto('/apps/dashboard') + await page.locator('header#header').waitFor({ state: 'visible' }) + await docElementScreenshot(page, 'header#header', 'user/webinterface_nav') +}) + +test('Unified search', async ({ page }) => { + await page.goto('/apps/dashboard') + await page.locator('header#header').waitFor({ state: 'visible' }) + await page.locator('#unified-search').click() + await page.locator('[data-cy-unified-search-filters]').waitFor({ state: 'visible', timeout: 10000 }) + await docScreenshot(page, 'user/webinterface_search') +}) + +test('Profile menu', async ({ page }) => { + await page.goto('/apps/dashboard') + await page.locator('header#header').waitFor({ state: 'visible' }) + await page.locator('#settings button, #user-menu button, header .user-status__status button, .user-status-menu-item button').first().click() + await page.locator('text=Log out').waitFor({ state: 'visible' }) + await docScreenshot(page, 'user/webinterface_profile_menu') +}) + +// Customise button (dashboard settings) +test('Customize button', async ({ page }) => { + await page.goto('/apps/dashboard') + await page.locator('.panel--header, .dashboard-widget-content').first().waitFor({ state: 'visible', timeout: 15000 }) + await page.locator('.icon-loading').waitFor({ state: 'hidden', timeout: 15000 }).catch(() => {}) + const customiseBtn = page.locator('button', { hasText: /custom[iz]/i }).first() + await customiseBtn.waitFor({ state: 'attached', timeout: 25000 }) + await customiseBtn.scrollIntoViewIfNeeded() + await customiseBtn.screenshot({ path: require('path').join(require('os').homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs', 'user', 'webinterface_customize_button.png') }) +}) diff --git a/playwright/fixtures/calendar/q3-gala.ics b/playwright/fixtures/calendar/q3-gala.ics new file mode 100644 index 00000000000..6abc323e0b2 --- /dev/null +++ b/playwright/fixtures/calendar/q3-gala.ics @@ -0,0 +1,27 @@ +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//NC Docs//Fixtures//EN +CALSCALE:GREGORIAN +METHOD:PUBLISH +BEGIN:VEVENT +UID:q3-gala-main-event-fixture@nextcloud-docs +SUMMARY:Q3 Fundraising Gala +DESCRIPTION:Annual fundraising gala at Riverside Pavilion.\nDinner\, keynote address\, auction and live music. +DTSTART:20260901T180000 +DTEND:20260901T230000 +LOCATION:Riverside Pavilion\, 12 Riverside Walk +ORGANIZER;CN=Christine Schott:mailto:c.schott@example.org +ATTENDEE;CN=Amara Winterbourne;RSVP=TRUE:mailto:amara@example.org +ATTENDEE;CN=Malik Santiago;RSVP=TRUE:mailto:malik@example.org +END:VEVENT +BEGIN:VEVENT +UID:q3-catering-walkthrough-fixture@nextcloud-docs +SUMMARY:Catering walkthrough +DESCRIPTION:On-site walkthrough with Greenleaf Catering. +DTSTART:20260822T100000 +DTEND:20260822T110000 +LOCATION:Riverside Pavilion +ORGANIZER;CN=Christine Schott:mailto:c.schott@example.org +ATTENDEE;CN=Lila Hawthorne;RSVP=TRUE:mailto:lila@example.org +END:VEVENT +END:VCALENDAR diff --git a/playwright/fixtures/contacts/team-contacts.vcf b/playwright/fixtures/contacts/team-contacts.vcf new file mode 100644 index 00000000000..fae037b3957 --- /dev/null +++ b/playwright/fixtures/contacts/team-contacts.vcf @@ -0,0 +1,36 @@ +BEGIN:VCARD +VERSION:3.0 +FN:Amara Winterbourne +N:Winterbourne;Amara;;; +ORG:Development Committee +TITLE:Event Coordinator +EMAIL;TYPE=WORK:amara@example.org +TEL;TYPE=WORK:+44 20 7946 0100 +END:VCARD +BEGIN:VCARD +VERSION:3.0 +FN:Lila Hawthorne +N:Hawthorne;Lila;;; +ORG:Greenleaf Catering Co. +TITLE:Account Manager +EMAIL;TYPE=WORK:lila@example.org +TEL;TYPE=WORK:+44 20 7946 0200 +END:VCARD +BEGIN:VCARD +VERSION:3.0 +FN:Malik Santiago +N:Santiago;Malik;;; +ORG:AV Solutions Ltd +TITLE:Senior Technician +EMAIL;TYPE=WORK:malik@example.org +TEL;TYPE=WORK:+44 20 7946 0300 +END:VCARD +BEGIN:VCARD +VERSION:3.0 +FN:Marlene Ashworth +N:Ashworth;Marlene;;; +ORG:Riverside Pavilion +TITLE:Events Manager +EMAIL;TYPE=WORK:marlene@riverside.example.org +TEL;TYPE=WORK:+44 20 7946 0400 +END:VCARD diff --git a/playwright/fixtures/documents/Budget Overview.csv b/playwright/fixtures/documents/Budget Overview.csv new file mode 100644 index 00000000000..b10aab68fbf --- /dev/null +++ b/playwright/fixtures/documents/Budget Overview.csv @@ -0,0 +1,18 @@ +Category,Item,Budgeted (£),Actual (£),Variance (£),Notes +Venue,Riverside Pavilion hire,4500,4500,0,Deposit paid +Venue,AV equipment & setup,1200,1150,50,Malik confirmed +Venue,Furniture & staging,600,620,-20,Extra tables added +Catering,Three-course dinner (250 pax),9000,8750,250,Final invoice pending +Catering,Welcome drinks & canapés,1800,1800,0, +Catering,Bar staff (5 hrs),600,600,0, +Entertainment,Live band,2200,2200,0,Signed contract +Entertainment,MC / host,500,500,0, +Entertainment,Lighting & effects,800,750,50, +Print & design,Printed programme (250 copies),350,320,30, +Print & design,Table centrepieces & signage,480,510,-30, +Marketing,Social media promotion,200,180,20, +Marketing,Email campaign,0,0,0,In-house +Staffing,Event volunteers (34 × expenses),680,640,40, +Staffing,Security (2 × 6 hrs),360,360,0, +Miscellaneous,Contingency,500,120,380, +Totals,,23770,23000,770, diff --git a/playwright/fixtures/documents/Event Brief.md b/playwright/fixtures/documents/Event Brief.md new file mode 100644 index 00000000000..94baa37e619 --- /dev/null +++ b/playwright/fixtures/documents/Event Brief.md @@ -0,0 +1,37 @@ +# Q3 Fundraising Gala — Event Brief + +**Date:** Saturday 1 September +**Venue:** Riverside Pavilion +**Expected attendance:** 250 guests + +## Overview + +The Q3 Fundraising Gala is our flagship annual event, bringing together donors, +volunteers, and community partners for an evening of dinner, music, and +recognition of the year's achievements. + +## Programme + +| Time | Activity | +|------|----------| +| 18:00 | Doors open / welcome drinks | +| 18:45 | Seated dinner | +| 20:15 | Keynote address | +| 20:45 | Auction | +| 21:30 | Live music & dancing | +| 23:00 | Close | + +## Key contacts + +- **Event lead:** Christine (organiser@example.org) +- **Venue liaison:** Marlene, Riverside Pavilion +- **Catering:** Greenleaf Catering Co. +- **AV:** Malik Santiago + +## Outstanding actions + +- [ ] Confirm final guest numbers with venue (due 15 Aug) +- [ ] Approve printed programme proof +- [ ] Brief volunteers on arrival procedures +- [x] Sponsor pack distributed +- [x] Ticket sales page live diff --git a/playwright/fixtures/documents/Volunteer List.csv b/playwright/fixtures/documents/Volunteer List.csv new file mode 100644 index 00000000000..741a4a86398 --- /dev/null +++ b/playwright/fixtures/documents/Volunteer List.csv @@ -0,0 +1,11 @@ +First name,Last name,Email,Role,Shift,Checked in +Amara,Winterbourne,amara@example.org,Lead coordinator,All day,Yes +Seraphina,Delgado,seraphina@example.org,Volunteer coordinator,All day,Yes +Lila,Hawthorne,lila@example.org,Registration desk,Morning,Yes +Kieran,Patel,kieran@example.org,Registration desk,Morning,Yes +Malik,Santiago,malik@example.org,AV support,All day,Yes +Charlotte,McGraw,charlotte@example.org,Finance desk,Evening,No +Orion,Gallagher,orion@example.org,Front of house,Evening,No +Adrian,Lelievre,adrian@example.org,Decoration setup,Morning,Yes +Analise,Laviss,analise@example.org,Guest relations,Evening,No +Benjamin,Clarke,ben@example.org,Parking marshal,Morning,Yes diff --git a/playwright/global-setup.ts b/playwright/global-setup.ts new file mode 100644 index 00000000000..b74ca973364 --- /dev/null +++ b/playwright/global-setup.ts @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { configureNextcloud, runOcc, startNextcloud, waitOnNextcloud } from '@nextcloud/e2e-test-server/docker' +import { login } from '@nextcloud/e2e-test-server/playwright' +import { User } from '@nextcloud/e2e-test-server' +import { chromium } from '@playwright/test' +import * as path from 'path' +import { SCREENSHOT_PORT } from '../playwright.config' +import { seed, seedNoteToSelf } from './seed' + +const SCREENSHOT_APPS = [ + 'activity', + 'calendar', + 'comments', + 'deck', + 'files_versions', + 'notes', + 'notifications', + 'spreed', + 'tasks', + 'viewer', +] + +const AUTH_FILE = path.join(__dirname, '.auth', 'state.json') + +export default async function globalSetup() { + await startNextcloud('stable33', false, { exposePort: SCREENSHOT_PORT }) + await waitOnNextcloud(`localhost:${SCREENSHOT_PORT}`) + await configureNextcloud(SCREENSHOT_APPS) + + // Enable pretty URLs so the e2e-test-server login() helper can verify + // authentication via /apps/files/ after login. Without this, the catch-all + // RewriteRule forwarding to index.php is missing from .htaccess and the + // URL is not routed through NC's front controller. + await runOcc(['config:system:set', 'htaccess.RewriteBase', '--value', '/']) + await runOcc(['maintenance:update:htaccess']) + // Disable brute-force protection so rapid login calls in beforeAll don't get + // throttled. Note: the key is all-lowercase "bruteforce", not camelCase. + await runOcc(['config:system:set', 'auth.bruteforce.protection.enabled', '--value', 'false', '--type', 'boolean']) + // Talk hides the "Message expiration" setting in conversation settings unless + // background jobs are in cron mode. Set it so the feature appears in the UI. + await runOcc(['config:app:set', 'core', 'backgroundjobs_mode', '--value', 'cron']) + + // Seed all users and Talk data via API. + // seedTalk() returns the "Event planning" group token needed for post-browser seeding. + const eventToken = await seed() + + // Launch a browser to initialise Talk (note-to-self requires a prior browser + // visit to /apps/spreed), seed post-browser data, then capture storageState so + // every test starts pre-authenticated without calling login() in beforeEach. + const browser = await chromium.launch() + const context = await browser.newContext({ + baseURL: `http://localhost:${SCREENSHOT_PORT}`, + userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36', + }) + const page = await context.newPage() + const christine = new User('christine', 'christine') + await login(page.request, christine) + await page.goto('/apps/spreed') + await page.locator('[aria-label="Conversation list"]').waitFor({ state: 'visible', timeout: 20000 }).catch(() => {}) + await seedNoteToSelf(eventToken) + await context.storageState({ path: AUTH_FILE }) + await browser.close() +} diff --git a/playwright/global-teardown.ts b/playwright/global-teardown.ts new file mode 100644 index 00000000000..6a56024e097 --- /dev/null +++ b/playwright/global-teardown.ts @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { stopNextcloud } from '@nextcloud/e2e-test-server/docker' +import { execSync } from 'child_process' +import * as os from 'os' +import * as path from 'path' + +export default async function globalTeardown() { + await stopNextcloud() + + // Compress screenshots with pngquant after all tests complete. + const screenshotDir = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs') + try { + const { default: pngquantBin } = await import('pngquant-bin') + execSync( + `find "${screenshotDir}" -name '*.png' -exec "${pngquantBin}" --quality=70-85 --force --ext .png --strip {} \\;`, + { stdio: 'inherit' }, + ) + } catch { + console.warn('pngquant compression failed — screenshots not compressed') + } +} diff --git a/playwright/helpers.ts b/playwright/helpers.ts new file mode 100644 index 00000000000..f9bfa6fe634 --- /dev/null +++ b/playwright/helpers.ts @@ -0,0 +1,170 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { Page } from '@playwright/test' +import { runOcc } from '@nextcloud/e2e-test-server/docker' +import { readFileSync } from 'fs' +import * as fs from 'fs/promises' +import * as path from 'path' +import * as os from 'os' + +export const SCREENSHOT_PORT = 8093 +export const BASE_URL = `http://localhost:${SCREENSHOT_PORT}/index.php` +const OCS_BASE = `http://localhost:${SCREENSHOT_PORT}` +const SCREENSHOT_DIR = path.join(os.homedir(), 'Pictures', 'Screenshots', 'nextcloud-docs') + +// ── Screenshot helpers ──────────────────────────────────────────────────────── + +async function suppressFocusRings(page: Page): Promise { + await page.addStyleTag({ content: [ + '*:focus, *:focus-visible, *:focus-within, *:has(:focus-visible) { outline: none !important; box-shadow: none !important; }', + '*:focus-visible + * { outline: none !important; box-shadow: none !important; }', + '::-webkit-scrollbar { display: none !important; }', + '* { scrollbar-width: none !important; }', + ].join('\n') }) +} + +/** + * Take a named viewport screenshot for documentation. + * Name mirrors the destination path relative to the manual root, + * e.g. 'user/files/sharing-dialog' → user_manual/files/images/sharing-dialog.png + */ +export async function docScreenshot( + page: Page, + name: string, + options: { clip?: { x: number; y: number; width: number; height: number } } = {}, +): Promise { + await suppressFocusRings(page) + await page.waitForTimeout(500) + const dest = path.join(SCREENSHOT_DIR, `${name}.png`) + await fs.mkdir(path.dirname(dest), { recursive: true }) + await page.screenshot({ path: dest, fullPage: false, ...options }) +} + +/** Take a screenshot of a specific element only. */ +export async function docElementScreenshot(page: Page, selector: string, name: string): Promise { + await suppressFocusRings(page) + await page.waitForTimeout(500) + const dest = path.join(SCREENSHOT_DIR, `${name}.png`) + await fs.mkdir(path.dirname(dest), { recursive: true }) + const element = page.locator(selector) + await element.waitFor({ state: 'visible' }) + await element.screenshot({ path: dest }) +} + +// ── OCC wrapper ─────────────────────────────────────────────────────────────── + +/** Split an occ command string respecting double-quoted tokens (e.g. --display-name="Amara W"). */ +function splitOcc(cmd: string): string[] { + const args: string[] = [] + let cur = '' + let inDouble = false + for (const ch of cmd) { + if (ch === '"') { inDouble = !inDouble } + else if (ch === ' ' && !inDouble) { if (cur) { args.push(cur); cur = '' } } + else { cur += ch } + } + if (cur) args.push(cur) + return args +} + +/** Run an occ command. Throws on non-zero exit. */ +export async function occ(cmd: string, env: Record = {}): Promise { + const envArray = Object.entries(env).map(([k, v]) => `${k}=${v}`) + return runOcc(splitOcc(cmd), { env: envArray }) +} + +/** Like occ() but swallows errors (e.g. "user already exists"). */ +export async function tryOcc(cmd: string, env: Record = {}): Promise { + try { return await occ(cmd, env) } catch { return null } +} + +// ── WebDAV / HTTP helpers ───────────────────────────────────────────────────── + +function basicAuth(user: string, password: string): string { + return 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64') +} + +export async function mkdavCol(dest: string, user: string, password: string): Promise { + await fetch(`${OCS_BASE}/remote.php/dav/files/${user}/${dest}`, { + method: 'MKCOL', + headers: { Authorization: basicAuth(user, password) }, + }) +} + +export async function uploadFile( + src: string, + dest: string, + user: string, + password: string, + mtime?: number, +): Promise { + const content = readFileSync(src) + const headers: Record = { Authorization: basicAuth(user, password) } + if (mtime) headers['X-OC-MTime'] = String(mtime) + await fetch(`${OCS_BASE}/remote.php/dav/files/${user}/${dest}`, { + method: 'PUT', + headers, + body: content, + }) +} + +export async function uploadAvatar(src: string, user: string, password: string): Promise { + const content = readFileSync(src) + const boundary = `----AvatarBoundary${Date.now()}` + const body = Buffer.concat([ + Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="files[]"; filename="avatar.png"\r\nContent-Type: image/png\r\n\r\n`), + content, + Buffer.from(`\r\n--${boundary}--\r\n`), + ]) + await fetch(`${OCS_BASE}/index.php/avatar`, { + method: 'POST', + headers: { + Authorization: basicAuth(user, password), + 'Content-Type': `multipart/form-data; boundary=${boundary}`, + 'OCS-APIREQUEST': 'true', + }, + body, + }) +} + +export async function ocsRequest( + method: string, + path: string, + user: string, + password: string, + body?: Record, +): Promise { + const headers: Record = { + Authorization: basicAuth(user, password), + 'OCS-APIRequest': 'true', + Accept: 'application/json', + } + const init: RequestInit = { method, headers } + if (body) { + headers['Content-Type'] = 'application/x-www-form-urlencoded' + init.body = new URLSearchParams(body) + } + return fetch(`${OCS_BASE}${path}`, init) +} + +export async function reactToMessage( + token: string, + messageId: number, + emoji: string, + user: string, + password: string, +): Promise { + await ocsRequest('POST', `/ocs/v2.php/apps/spreed/api/v1/reaction/${token}/${messageId}`, user, password, { reaction: emoji }) +} + +export async function seedChatMessages( + token: string, + messages: Array<{ text: string; user: string; password: string }>, +): Promise { + for (const msg of messages) { + await ocsRequest('POST', `/ocs/v2.php/apps/spreed/api/v1/chat/${token}`, msg.user, msg.password, { + message: msg.text, + }) + } +} diff --git a/playwright/seed/files.ts b/playwright/seed/files.ts new file mode 100644 index 00000000000..5c4a29e0a20 --- /dev/null +++ b/playwright/seed/files.ts @@ -0,0 +1,169 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { mkdavCol, uploadFile, ocsRequest, SCREENSHOT_PORT } from '../helpers' +import * as path from 'path' + +const FIXTURES_DIR = path.join(__dirname, '..', 'fixtures') +const daysAgo = (n: number) => Math.floor(Date.now() / 1000 - n * 86400) + +async function share(filePath: string, user: string, password: string, shareType: string, opts: Record = {}): Promise { + await ocsRequest('POST', '/ocs/v2.php/apps/files_sharing/api/v1/shares', user, password, { + path: filePath, + shareType, + ...opts, + }).catch(() => {}) +} + +async function davExists(davPath: string, user: string, password: string): Promise { + const res = await fetch(`http://localhost:${SCREENSHOT_PORT}/remote.php/dav/files/${user}/${davPath}`, { + method: 'PROPFIND', + headers: { + Authorization: 'Basic ' + Buffer.from(`${user}:${password}`).toString('base64'), + Depth: '0', + }, + }) + return res.status === 207 +} + +async function uploadIfMissing(src: string, dest: string, user: string, password: string, mtime?: number): Promise { + if (!await davExists(dest, user, password)) { + await uploadFile(src, dest, user, password, mtime) + } +} + +export async function seedFiles(): Promise { + // ── Christine's folder tree ─────────────────────────────────────────────── + + await mkdavCol('Documents', 'christine', 'christine') + await mkdavCol('Photos', 'christine', 'christine') + await mkdavCol('Projects', 'christine', 'christine') + await mkdavCol('Projects/Q3 Gala', 'christine', 'christine') + + // Documents — text, spreadsheet, contacts, calendar + await uploadIfMissing( + `${FIXTURES_DIR}/documents/Event Brief.md`, + 'Documents/Event Brief.md', + 'christine', 'christine', + daysAgo(7), + ) + await uploadIfMissing( + `${FIXTURES_DIR}/documents/Budget Overview.csv`, + 'Documents/Budget Overview.csv', + 'christine', 'christine', + daysAgo(12), + ) + await uploadIfMissing( + `${FIXTURES_DIR}/documents/Volunteer List.csv`, + 'Documents/Volunteer List.csv', + 'christine', 'christine', + daysAgo(5), + ) + await uploadIfMissing( + `${FIXTURES_DIR}/contacts/team-contacts.vcf`, + 'Documents/team-contacts.vcf', + 'christine', 'christine', + daysAgo(20), + ) + await uploadIfMissing( + `${FIXTURES_DIR}/calendar/q3-gala.ics`, + 'Documents/q3-gala.ics', + 'christine', 'christine', + daysAgo(18), + ) + + // Photos — landscape JPEGs for gallery / image preview screenshots + await uploadIfMissing( + `${FIXTURES_DIR}/images/forest-green.jpg`, + 'Photos/forest-green.jpg', + 'christine', 'christine', + daysAgo(14), + ) + await uploadIfMissing( + `${FIXTURES_DIR}/images/ocean-golden.jpg`, + 'Photos/ocean-golden.jpg', + 'christine', 'christine', + daysAgo(11), + ) + await uploadIfMissing( + `${FIXTURES_DIR}/images/city-night-purple.jpg`, + 'Photos/city-night-purple.jpg', + 'christine', 'christine', + daysAgo(9), + ) + await uploadIfMissing( + `${FIXTURES_DIR}/images/milky-way.jpg`, + 'Photos/milky-way.jpg', + 'christine', 'christine', + daysAgo(9), + ) + + // Projects — PDFs + await uploadIfMissing( + `${FIXTURES_DIR}/pdfs/Q2 Project Proposal.pdf`, + 'Projects/Q3 Gala/Q2 Project Proposal.pdf', + 'christine', 'christine', + daysAgo(21), + ) + await uploadIfMissing( + `${FIXTURES_DIR}/pdfs/Team Meeting Notes.pdf`, + 'Projects/Q3 Gala/Team Meeting Notes.pdf', + 'christine', 'christine', + daysAgo(3), + ) + + // ── Shares from christine ───────────────────────────────────────────────── + + // Share Projects/Q3 Gala/ folder with amara_w (editor: read + create + update) + await share('/Projects/Q3 Gala', 'christine', 'christine', '0', { + shareWith: 'amara_w', + permissions: '17', + }) + + // Share Documents/ folder with malik_s (read-only) + await share('/Documents', 'christine', 'christine', '0', { + shareWith: 'malik_s', + permissions: '1', + }) + + // Share Team Meeting Notes.pdf with lila_h (read-only) + await share('/Projects/Q3 Gala/Team Meeting Notes.pdf', 'christine', 'christine', '0', { + shareWith: 'lila_h', + permissions: '1', + }) + + // Public link share on Q2 Project Proposal.pdf (read-only) + await share('/Projects/Q3 Gala/Q2 Project Proposal.pdf', 'christine', 'christine', '3', { + permissions: '1', + }) + + // ── amara_w's files ─────────────────────────────────────────────────────── + + // amara_w may already have these from Talk DM seeding; upload if missing + await uploadIfMissing( + `${FIXTURES_DIR}/pdfs/Q2 Project Proposal.pdf`, + 'Q2 Project Proposal.pdf', + 'amara_w', 'amara_w', + daysAgo(21), + ) + await uploadIfMissing( + `${FIXTURES_DIR}/documents/Budget Overview.csv`, + 'Budget Overview.csv', + 'amara_w', 'amara_w', + daysAgo(12), + ) + + // Share Q2 Project Proposal.pdf from amara_w back to christine (read-only) + // so christine's "Shared with you" view has content + await share('/Q2 Project Proposal.pdf', 'amara_w', 'amara_w', '0', { + shareWith: 'christine', + permissions: '1', + }) + + // amara_w also shares the budget CSV with lila_h — gives lila_h a realistic + // "Shared with you" list for her own account + await share('/Budget Overview.csv', 'amara_w', 'amara_w', '0', { + shareWith: 'lila_h', + permissions: '1', + }) +} diff --git a/playwright/seed/index.ts b/playwright/seed/index.ts new file mode 100644 index 00000000000..c3dc8bcf841 --- /dev/null +++ b/playwright/seed/index.ts @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +export { seedUsers } from './users' +export { seedTalk, seedNoteToSelf, adjustTalkTimestamps } from './talk' +export { seedFiles } from './files' + +import { seedUsers } from './users' +import { seedTalk, adjustTalkTimestamps } from './talk' +import { seedFiles } from './files' + +/** + * Run all pre-browser seeding: users, Talk rooms, messages, files, shares. + * Returns the "Event planning" group token needed for post-browser seeding. + */ +export async function seed(): Promise { + await seedUsers() + const tokens = await seedTalk() + await seedFiles() + await adjustTalkTimestamps(tokens) + return tokens.event +} diff --git a/playwright/seed/talk.ts b/playwright/seed/talk.ts new file mode 100644 index 00000000000..2f0688bc7d9 --- /dev/null +++ b/playwright/seed/talk.ts @@ -0,0 +1,395 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { ocsRequest, seedChatMessages, reactToMessage, uploadFile, SCREENSHOT_PORT } from '../helpers' +import { runExec } from '@nextcloud/e2e-test-server/docker' +import * as path from 'path' + +const FIXTURES_DIR = path.join(__dirname, '..', 'fixtures') + +const SEC = 1 +const DAY = 86400 * SEC +const daysAgo = (n: number) => Math.floor(Date.now() / 1000 - n * DAY) + +// ── Low-level helpers ───────────────────────────────────────────────────────── + +function talkCall(method: string, talkPath: string, user: string, password: string, body?: Record) { + return ocsRequest(method, `/ocs/v2.php/apps/spreed/api${talkPath}`, user, password, body) +} + +async function createGroup(name: string): Promise { + const res = await talkCall('POST', '/v4/room', 'christine', 'christine', { roomType: '2', roomName: name }) + const data = await res.json() + return data.ocs.data.token as string +} + +async function addParticipant(token: string, uid: string): Promise { + await talkCall('POST', `/v4/room/${token}/participants`, 'christine', 'christine', { newParticipant: uid, source: 'users' }) +} + +async function createDm(target: string): Promise { + const res = await talkCall('POST', '/v4/room', 'christine', 'christine', { roomType: '1', invite: target }) + const data = await res.json() + return data.ocs.data.token as string +} + +/** + * Backdate a room's last_activity in oc_talk_rooms to endTs so the conversation + * list shows a realistic relative timestamp (e.g. "3 days ago"). + * + * The Talk API has no parameter for this, so we write directly to SQLite via + * PHP PDO. SQL is base64-encoded to avoid shell-escaping issues. + * + * Two caveats handled here: + * 1. oc_talk_rooms.last_activity is a DATETIME column ("YYYY-MM-DD HH:MM:SS"). + * Writing a raw Unix integer causes Talk's PHP ORM to throw when parsing it, + * returning HTTP 500 for the rooms list. Use SQLite datetime() to format correctly. + * 2. Talk's getRooms API filters rooms by modifiedSince (last poll timestamp). + * Backdating last_activity makes rooms fail this filter. Setting Christine's + * last_attendee_activity to 2 h in the future ensures the attendee-activity + * fallback check always passes: room included when EITHER last_activity OR + * lastAttendeeActivity >= modifiedSince. + */ +async function spreadTimestamps(token: string, _startTs: number, endTs: number): Promise { + // Seed time is before tests run; add 2 h so last_attendee_activity stays > + // modifiedSince for any test execution within 2 hours of seeding. + const futureTs = Math.floor(Date.now() / 1000) + 7200 + // Backdate room timestamp for conversation-list display. + // oc_talk_rooms.last_activity is a DATETIME column (stored as "YYYY-MM-DD HH:MM:SS"). + // SQLite datetime() converts the Unix timestamp to the correct string format. + const sql1 = `UPDATE oc_talk_rooms SET last_activity = datetime(${endTs}, 'unixepoch') WHERE token = '${token}'` + // Keep Christine's attendee record fresh so modifiedSince polling still returns this room + const sql2 = `UPDATE oc_talk_attendees SET last_attendee_activity = ${futureTs} WHERE room_id = (SELECT id FROM oc_talk_rooms WHERE token = '${token}') AND actor_id = 'christine'` + const b64_1 = Buffer.from(sql1).toString('base64') + const b64_2 = Buffer.from(sql2).toString('base64') + const php = `try{$db=new PDO('sqlite:/var/www/html/data/owncloud.db');$db->exec(base64_decode('${b64_1}'));$db->exec(base64_decode('${b64_2}'));echo"ok";}catch(Exception $e){echo"PDO_ERR:".$e->getMessage();}` + const out = await runExec(['php', '-r', php]).catch((e: Error) => `EXEC_ERR:${e.message}`) + if (!String(out).startsWith('ok')) console.warn(`[spreadTimestamps] ${token}: ${String(out)}`) +} + +// ── Seed functions ──────────────────────────────────────────────────────────── + +interface DmTokens { + amara: string + charlotte: string + orion: string + adrian: string +} + +async function seedDms(): Promise { + // amara_w ↔ christine + const amara = await createDm('amara_w') + const chatRes = await talkCall('GET', `/v1/chat/${amara}?lookIntoFuture=0&limit=20`, 'christine', 'christine') + const chatData = await chatRes.json() + const msgs: Array<{ systemMessage?: string }> = chatData?.ocs?.data ?? [] + if (msgs.filter(m => !m.systemMessage).length === 0) { + await seedChatMessages(amara, [ + { text: 'Do you have a minute?', user: 'amara_w', password: 'amara_w' }, + { text: "Absolutely, what's up?", user: 'christine', password: 'christine' }, + { text: "The client got back to me — they're considering joining the fundraising next Thursday if we can secure a round table. Can you help?", user: 'amara_w', password: 'amara_w' }, + { text: "Great news! Have you already spoken to Marlene at the venue about adding a round table?", user: 'christine', password: 'christine' }, + { text: "Marlene said it'd be tricky this close to the date but she'll try. Might need an escalation.", user: 'amara_w', password: 'amara_w' }, + { text: "I'll contact them straight away to make sure we can accommodate the client. Thanks for looping me in!", user: 'christine', password: 'christine' }, + { text: "Wonderful, thank you so much! 🙌", user: 'amara_w', password: 'amara_w' }, + { text: "Happy to help! Let me know how it goes.", user: 'christine', password: 'christine' }, + { text: "Will do. Also — I've shared the Q2 proposal and meeting notes in this chat for your reference.", user: 'amara_w', password: 'amara_w' }, + ]) + await uploadFile(`${FIXTURES_DIR}/pdfs/Q2 Project Proposal.pdf`, 'Q2 Project Proposal.pdf', 'amara_w', 'amara_w') + await ocsRequest('POST', '/ocs/v2.php/apps/files_sharing/api/v1/shares', 'amara_w', 'amara_w', { + shareType: '10', path: '/Q2 Project Proposal.pdf', shareWith: amara, + }) + await uploadFile(`${FIXTURES_DIR}/pdfs/Team Meeting Notes.pdf`, 'Team Meeting Notes.pdf', 'amara_w', 'amara_w') + await ocsRequest('POST', '/ocs/v2.php/apps/files_sharing/api/v1/shares', 'amara_w', 'amara_w', { + shareType: '10', path: '/Team Meeting Notes.pdf', shareWith: amara, + }) + await seedChatMessages(amara, [ + { text: "Perfect, I'll review them before our call.", user: 'christine', password: 'christine' }, + ]) + const allMsgsRes = await talkCall('GET', `/v1/chat/${amara}?lookIntoFuture=0&limit=30`, 'christine', 'christine') + const allMsgsData = await allMsgsRes.json() + const allMsgs: Array<{ id: number; message: string }> = allMsgsData?.ocs?.data ?? [] + for (const msg of allMsgs) { + if (msg.message.includes('Great news')) { + await reactToMessage(amara, msg.id, '👍', 'amara_w', 'amara_w').catch(() => {}) + await reactToMessage(amara, msg.id, '❤️', 'lila_h', 'lila_h').catch(() => {}) + } + if (msg.message.includes('Happy to help')) { + await reactToMessage(amara, msg.id, '🙏', 'amara_w', 'amara_w').catch(() => {}) + } + } + } + + // charlotte_m ↔ christine + const charlotte = await createDm('charlotte_m') + const charlotteChatRes = await talkCall('GET', `/v1/chat/${charlotte}?lookIntoFuture=0&limit=20`, 'christine', 'christine') + const charlotteChatData = await charlotteChatRes.json() + const charlotteMsgs: Array<{ systemMessage?: string }> = charlotteChatData?.ocs?.data ?? [] + if (charlotteMsgs.filter(m => !m.systemMessage).length === 0) { + await seedChatMessages(charlotte, [ + { text: "Hi Christine — the venue is asking for the £2,500 deposit by end of week. Shall I go ahead and authorise it?", user: 'charlotte_m', password: 'charlotte_m' }, + { text: "Yes, please go ahead — I've already confirmed it with finance.", user: 'christine', password: 'christine' }, + { text: "Perfect. I'll send the invoice to accounts once it's done.", user: 'charlotte_m', password: 'charlotte_m' }, + ]) + } + + // orion_g ↔ christine + const orion = await createDm('orion_g') + const orionChatRes = await talkCall('GET', `/v1/chat/${orion}?lookIntoFuture=0&limit=20`, 'christine', 'christine') + const orionChatData = await orionChatRes.json() + const orionMsgs: Array<{ systemMessage?: string }> = orionChatData?.ocs?.data ?? [] + if (orionMsgs.filter(m => !m.systemMessage).length === 0) { + await seedChatMessages(orion, [ + { text: "Just saw your post about the gala — looks amazing! 🎉", user: 'orion_g', password: 'orion_g' }, + { text: "Thanks Orion! It's shaping up really well. Tickets go on sale next month.", user: 'christine', password: 'christine' }, + { text: "@christine are you free Thursday for a quick call on ticketing?", user: 'orion_g', password: 'orion_g' }, + ]) + } + + // adrian_l ↔ christine + const adrian = await createDm('adrian_l') + const adrianChatRes = await talkCall('GET', `/v1/chat/${adrian}?lookIntoFuture=0&limit=20`, 'christine', 'christine') + const adrianChatData = await adrianChatRes.json() + const adrianMsgs: Array<{ systemMessage?: string }> = adrianChatData?.ocs?.data ?? [] + if (adrianMsgs.filter(m => !m.systemMessage).length === 0) { + await seedChatMessages(adrian, [ + { text: "Christine, just confirming — are the decorators booked for the 1st?", user: 'adrian_l', password: 'adrian_l' }, + ]) + } + + return { amara, charlotte, orion, adrian } +} + +interface GroupTokens { + event: string + design: string + updates: string + board: string + volunteer: string +} + +async function seedEventPlanningGroup(): Promise { + const listRes = await talkCall('GET', '/v4/room', 'christine', 'christine') + const listData = await listRes.json() + const rooms: Array<{ token: string; displayName: string; isArchived?: boolean }> = listData?.ocs?.data ?? [] + const existing = rooms.find(r => r.displayName === 'Event planning') + + let token: string + if (existing) { + if (existing.isArchived) { + await talkCall('DELETE', `/v4/room/${existing.token}/archive`, 'christine', 'christine') + } + token = existing.token + } else { + token = await createGroup('Event planning') + await addParticipant(token, 'amara_w') + await seedChatMessages(token, [ + { text: "Hi team! I've set up this conversation for coordinating the Q3 fundraising event.", user: 'christine', password: 'christine' }, + { text: 'Great, thanks for setting this up! I have a few updates to share.', user: 'amara_w', password: 'amara_w' }, + { text: "Looking forward to hearing them. Let's get started!", user: 'christine', password: 'christine' }, + ]) + } + + await talkCall('POST', `/v1/room/${token}/avatar/emoji`, 'christine', 'christine', { emoji: '🎪', color: '0082c9' }).catch(() => {}) + + const grpChatRes = await talkCall('GET', `/v1/chat/${token}?lookIntoFuture=0&limit=20`, 'christine', 'christine') + const grpChatData = await grpChatRes.json() + const grpMsgs: Array<{ id: number; message: string; systemMessage?: string }> = grpChatData?.ocs?.data ?? [] + if (grpMsgs.filter(m => !m.systemMessage && m.message && !m.message.startsWith('{')).length <= 3) { + await seedChatMessages(token, [ + { text: "Quick update: Riverside Pavilion confirmed for 1 September! 🎉", user: 'christine', password: 'christine' }, + { text: "Amazing! I've already started the sponsor outreach — three leads so far.", user: 'amara_w', password: 'amara_w' }, + { text: "That's great progress. Malik, can you handle the AV quote this week?", user: 'christine', password: 'christine' }, + { text: "On it — I'll have something to you by Thursday.", user: 'malik_s', password: 'malik_s' }, + { text: "Thanks everyone. Reminder: catering walkthrough is Friday at 10am.", user: 'christine', password: 'christine' }, + { text: "I'll be there!", user: 'amara_w', password: 'amara_w' }, + { text: "Me too 👍", user: 'malik_s', password: 'malik_s' }, + ]) + const freshRes = await talkCall('GET', `/v1/chat/${token}?lookIntoFuture=0&limit=20`, 'christine', 'christine') + const freshData = await freshRes.json() + const freshMsgs: Array<{ id: number; message: string }> = freshData?.ocs?.data ?? [] + for (const msg of freshMsgs) { + if (msg.message.includes('Riverside Pavilion confirmed')) { + await reactToMessage(token, msg.id, '🎉', 'amara_w', 'amara_w').catch(() => {}) + await reactToMessage(token, msg.id, '🎉', 'malik_s', 'malik_s').catch(() => {}) + await reactToMessage(token, msg.id, '👏', 'lila_h', 'lila_h').catch(() => {}) + } + } + } + + return token +} + +async function seedAdditionalGroups(existingRooms: Array<{ displayName: string; token: string }>): Promise> { + const byName = Object.fromEntries(existingRooms.map(r => [r.displayName, r.token])) + + let design = byName['Design Team'] ?? '' + if (!design) { + design = await createGroup('Design Team') + await talkCall('POST', `/v1/room/${design}/avatar/emoji`, 'christine', 'christine', { emoji: '🎨', color: 'a3174b' }).catch(() => {}) + await addParticipant(design, 'lila_h') + await addParticipant(design, 'kieran_p') + await seedChatMessages(design, [ + { text: "Hey team! Sharing the updated brand kit for the gala — new colour palette and logo lockups.", user: 'christine', password: 'christine' }, + { text: "Love the new palette! The deep teal works really well for the event signage.", user: 'lila_h', password: 'lila_h' }, + { text: "Agreed. Kieran, can you update the social templates once you have a moment?", user: 'christine', password: 'christine' }, + { text: "Sure, I'll have the Instagram and LinkedIn versions ready by end of day.", user: 'kieran_p', password: 'kieran_p' }, + ]) + } + + let updates = byName['Project Updates'] ?? '' + if (!updates) { + updates = await createGroup('Project Updates') + await talkCall('POST', `/v1/room/${updates}/avatar/emoji`, 'christine', 'christine', { emoji: '📢', color: 'e9a227' }).catch(() => {}) + await addParticipant(updates, 'amara_w') + await addParticipant(updates, 'malik_s') + await addParticipant(updates, 'lila_h') + await addParticipant(updates, 'seraphina_d') + await seedChatMessages(updates, [ + { text: "📅 Gala planning is on track. Key milestone: venue confirmed for 1 Sep.", user: 'christine', password: 'christine' }, + { text: "Ticket sales open 1 July — please share the link with your networks!", user: 'christine', password: 'christine' }, + { text: "Will do! Already have a few colleagues who are interested.", user: 'seraphina_d', password: 'seraphina_d' }, + { text: "Sponsor pack v2 is out — thanks Amara for the quick turnaround.", user: 'christine', password: 'christine' }, + { text: "Happy to help. Three warm leads already replied!", user: 'amara_w', password: 'amara_w' }, + ]) + } + + let board = byName['Board Updates'] ?? '' + if (!board) { + board = await createGroup('Board Updates') + await talkCall('POST', `/v1/room/${board}/avatar/emoji`, 'christine', 'christine', { emoji: '📋', color: '003b6f' }).catch(() => {}) + await addParticipant(board, 'analise_l') + await addParticipant(board, 'orion_g') + await addParticipant(board, 'charlotte_m') + await seedChatMessages(board, [ + { text: "Minutes from the last board meeting have been uploaded to the shared folder.", user: 'christine', password: 'christine' }, + { text: "Charlotte, can you confirm the financials are signed off before the next session?", user: 'charlotte_m', password: 'charlotte_m' }, + { text: "Reviewed and signed off ✅", user: 'analise_l', password: 'analise_l' }, + ]) + } + + let volunteer = byName['Volunteer Coordination'] ?? '' + if (!volunteer) { + volunteer = await createGroup('Volunteer Coordination') + await talkCall('POST', `/v1/room/${volunteer}/avatar/emoji`, 'christine', 'christine', { emoji: '🤝', color: '00a75c' }).catch(() => {}) + await addParticipant(volunteer, 'analise_l') + await addParticipant(volunteer, 'seraphina_d') + await seedChatMessages(volunteer, [ + { text: "34 volunteers confirmed for the event day — great response!", user: 'seraphina_d', password: 'seraphina_d' }, + { text: "@christine we still need 6 more for the morning setup shift.", user: 'analise_l', password: 'analise_l' }, + ]) + } + + return { design, updates, board, volunteer } +} + +function fmtUtc(d: Date): string { + return d.getUTCFullYear().toString() + + String(d.getUTCMonth() + 1).padStart(2, '0') + + String(d.getUTCDate()).padStart(2, '0') + 'T' + + String(d.getUTCHours()).padStart(2, '0') + + String(d.getUTCMinutes()).padStart(2, '0') + + String(d.getUTCSeconds()).padStart(2, '0') + 'Z' +} + +async function seedCalendarEvent(eventToken: string): Promise { + const meetStart = new Date(Date.now() + 24 * 3600 * 1000) + meetStart.setUTCHours(10, 0, 0, 0) + const meetEnd = new Date(meetStart.getTime() + 3600000) + const ics = [ + 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//NC Docs//Seed//EN', + 'BEGIN:VEVENT', + 'UID:event-planning-catchup-docs-seed', + `DTSTART:${fmtUtc(meetStart)}`, + `DTEND:${fmtUtc(meetEnd)}`, + 'SUMMARY:Event planning catchup', + `LOCATION:http://localhost:${SCREENSHOT_PORT}/call/${eventToken}`, + 'END:VEVENT', 'END:VCALENDAR', + ].join('\r\n') + await fetch(`http://localhost:${SCREENSHOT_PORT}/remote.php/dav/calendars/christine/personal/event-planning-catchup-docs-seed.ics`, { + method: 'PUT', + headers: { + Authorization: 'Basic ' + Buffer.from('christine:christine').toString('base64'), + 'Content-Type': 'text/calendar; charset=utf-8', + }, + body: ics, + }).catch(() => {}) +} + +/** + * Backdate message timestamps for all seeded rooms so that conversations + * appear to have taken place over realistic timeframes rather than all + * within the same second. Must run after all messages are seeded. + */ +export async function adjustTalkTimestamps(tokens: GroupTokens & DmTokens): Promise { + await spreadTimestamps(tokens.amara, daysAgo(14), daysAgo(2)) + await spreadTimestamps(tokens.charlotte, daysAgo(6), daysAgo(4)) + await spreadTimestamps(tokens.orion, daysAgo(3), daysAgo(2)) + await spreadTimestamps(tokens.adrian, daysAgo(1), daysAgo(1)) + await spreadTimestamps(tokens.event, daysAgo(21), daysAgo(1)) + await spreadTimestamps(tokens.design, daysAgo(8), daysAgo(5)) + await spreadTimestamps(tokens.updates, daysAgo(6), daysAgo(3)) + await spreadTimestamps(tokens.board, daysAgo(15), daysAgo(9)) + await spreadTimestamps(tokens.volunteer, daysAgo(4), daysAgo(1)) +} + +/** + * Seed all Talk data that can be created via API before any browser session. + * Returns all room tokens for use in adjustTalkTimestamps and seedNoteToSelf. + */ +export async function seedTalk(): Promise { + const dmTokens = await seedDms() + const event = await seedEventPlanningGroup() + + // Fetch room list once for seedAdditionalGroups to look up existing tokens + const allRoomsRes = await talkCall('GET', '/v4/room', 'christine', 'christine') + const allRoomsData = await allRoomsRes.json() + const allRooms: Array<{ displayName: string; token: string }> = allRoomsData?.ocs?.data ?? [] + const groupTokens = await seedAdditionalGroups(allRooms) + + await seedCalendarEvent(event) + + return { event, ...dmTokens, ...groupTokens } +} + +/** + * Seed data that requires the browser to have visited /apps/spreed first. + * Call this in global-setup after the browser has navigated to Talk. + */ +export async function seedNoteToSelf(eventToken: string): Promise { + // Note-to-self task list + const noteRes = await talkCall('GET', '/v1/note-to-self', 'christine', 'christine') + const noteData = await noteRes.json() + const noteToken = noteData?.ocs?.data?.token as string | undefined + if (noteToken) { + const noteChatRes = await talkCall('GET', `/v1/chat/${noteToken}?lookIntoFuture=0&limit=50`, 'christine', 'christine') + const noteChatData = await noteChatRes.json() + const noteMsgs: Array<{ message?: string; systemMessage?: string }> = noteChatData?.ocs?.data ?? [] + if (!noteMsgs.some(m => m.message?.includes('Define Project Scope'))) { + await seedChatMessages(noteToken, [{ + text: '- [x] Define Project Scope and Objectives\n- [x] Develop a Project Plan\n- [ ] Coordinate Team Activities\n- [ ] Review and finalize budget\n- [ ] Schedule kickoff meeting', + user: 'christine', + password: 'christine', + }]) + } + } + + // Reminders on DM and group messages for the Talk dashboard panel + const dmToken = await createDm('amara_w') + const dmChatRes = await talkCall('GET', `/v1/chat/${dmToken}?lookIntoFuture=0&limit=20`, 'christine', 'christine') + const dmChatData = await dmChatRes.json() + const dmMsgs: Array<{ id: number; message?: string; systemMessage?: string }> = dmChatData?.ocs?.data ?? [] + const dmReminderMsg = dmMsgs.find(m => !m.systemMessage && m.message?.includes('Q2 proposal')) + if (dmReminderMsg) { + const inTwoDays = Math.floor(Date.now() / 1000) + 2 * 24 * 3600 + await talkCall('POST', `/v1/chat/${dmToken}/${dmReminderMsg.id}/reminder`, 'christine', 'christine', { timestamp: String(inTwoDays) }).catch(() => {}) + } + + const grpChatRes = await talkCall('GET', `/v1/chat/${eventToken}?lookIntoFuture=0&limit=20`, 'christine', 'christine') + const grpChatData = await grpChatRes.json() + const grpMsgs: Array<{ id: number; message?: string; systemMessage?: string }> = grpChatData?.ocs?.data ?? [] + const grpReminderMsg = grpMsgs.find(m => !m.systemMessage && m.message?.includes('catering walkthrough')) + if (grpReminderMsg) { + const tomorrow = Math.floor(Date.now() / 1000) + 24 * 3600 + await talkCall('POST', `/v1/chat/${eventToken}/${grpReminderMsg.id}/reminder`, 'christine', 'christine', { timestamp: String(tomorrow) }).catch(() => {}) + } +} diff --git a/playwright/seed/users.ts b/playwright/seed/users.ts new file mode 100644 index 00000000000..d4c66588ec3 --- /dev/null +++ b/playwright/seed/users.ts @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: AGPL-3.0-or-later + +import { tryOcc, uploadAvatar, ocsRequest } from '../helpers' + +const AVATAR_DIR = '/home/anna/Downloads/tp/avatar' + +/** Set a profile field (org, role, phone, etc.) as the user themselves. */ +async function setProfileField(userId: string, key: string, value: string): Promise { + await ocsRequest('PUT', `/ocs/v2.php/cloud/users/${userId}`, userId, userId, { key, value }) +} + +/** Set email via admin — users cannot change their own email through the OCS API. */ +async function setEmail(userId: string, email: string): Promise { + await ocsRequest('PUT', `/ocs/v2.php/cloud/users/${userId}`, 'admin', 'admin', { key: 'email', value: email }) +} + +export async function seedUsers(): Promise { + await tryOcc('user:add --password-from-env --display-name="Christine Schott" christine', { OC_PASS: 'christine' }) + await uploadAvatar(`${AVATAR_DIR}/christine/avatar.png`, 'christine', 'christine') + await setEmail('christine', 'c.schott@example.org') + await setProfileField('christine', 'organisation', 'Charity Events Foundation') + await setProfileField('christine', 'role', 'Event Manager') + await setProfileField('christine', 'phone', '+44 20 7946 0001') + + await tryOcc('user:add --password-from-env --display-name="Amara Winterbourne" amara_w', { OC_PASS: 'amara_w' }) + await uploadAvatar(`${AVATAR_DIR}/amara_w/avatar.png`, 'amara_w', 'amara_w') + await setEmail('amara_w', 'amara@example.org') + await setProfileField('amara_w', 'organisation', 'Development Committee') + await setProfileField('amara_w', 'role', 'Event Coordinator') + await setProfileField('amara_w', 'phone', '+44 20 7946 0100') + + await tryOcc('user:add --password-from-env --display-name="Lila Hawthorne" lila_h', { OC_PASS: 'lila_h' }) + await uploadAvatar(`${AVATAR_DIR}/Lila_Hawthorne/avatar.png`, 'lila_h', 'lila_h') + await setEmail('lila_h', 'lila@example.org') + await setProfileField('lila_h', 'organisation', 'Greenleaf Catering Co.') + await setProfileField('lila_h', 'role', 'Account Manager') + + await tryOcc('user:add --password-from-env --display-name="Malik Santiago" malik_s', { OC_PASS: 'malik_s' }) + await uploadAvatar(`${AVATAR_DIR}/Malik_Santiago/avatar.png`, 'malik_s', 'malik_s') + await setEmail('malik_s', 'malik@example.org') + await setProfileField('malik_s', 'organisation', 'AV Solutions Ltd') + await setProfileField('malik_s', 'role', 'Senior Technician') + + await tryOcc('user:add --password-from-env --display-name="Kieran Patel" kieran_p', { OC_PASS: 'kieran_p' }) + await uploadAvatar(`${AVATAR_DIR}/Kieran_Patel/avatar.png`, 'kieran_p', 'kieran_p') + await setEmail('kieran_p', 'kieran@example.org') + await setProfileField('kieran_p', 'organisation', 'Charity Events Foundation') + await setProfileField('kieran_p', 'role', 'Graphic Designer') + + await tryOcc('user:add --password-from-env --display-name="Seraphina Delgado" seraphina_d', { OC_PASS: 'seraphina_d' }) + await uploadAvatar(`${AVATAR_DIR}/Seraphina_Delgado/avatar.png`, 'seraphina_d', 'seraphina_d') + await setEmail('seraphina_d', 'seraphina@example.org') + await setProfileField('seraphina_d', 'organisation', 'Charity Events Foundation') + await setProfileField('seraphina_d', 'role', 'Volunteer Coordinator') + + await tryOcc('user:add --password-from-env --display-name="Adrian Lelievre" adrian_l', { OC_PASS: 'adrian_l' }) + await uploadAvatar(`${AVATAR_DIR}/Adrian_Lelievre/avatar.png`, 'adrian_l', 'adrian_l') + await setEmail('adrian_l', 'adrian@example.org') + await setProfileField('adrian_l', 'organisation', 'Riverside Pavilion') + await setProfileField('adrian_l', 'role', 'Venue Decorator') + + await tryOcc('user:add --password-from-env --display-name="Charlotte McGraw" charlotte_m', { OC_PASS: 'charlotte_m' }) + await uploadAvatar(`${AVATAR_DIR}/CharlotteMcGraw/avatar.png`, 'charlotte_m', 'charlotte_m') + await setEmail('charlotte_m', 'charlotte@example.org') + await setProfileField('charlotte_m', 'organisation', 'Charity Events Foundation') + await setProfileField('charlotte_m', 'role', 'Finance Officer') + + await tryOcc('user:add --password-from-env --display-name="Orion Gallagher" orion_g', { OC_PASS: 'orion_g' }) + await uploadAvatar(`${AVATAR_DIR}/Orion_Gallagher/avatar.png`, 'orion_g', 'orion_g') + await setEmail('orion_g', 'orion@example.org') + await setProfileField('orion_g', 'organisation', 'Charity Events Foundation') + await setProfileField('orion_g', 'role', 'Ticketing Lead') + + await tryOcc('user:add --password-from-env --display-name="Analise Laviss" analise_l', { OC_PASS: 'analise_l' }) + await uploadAvatar(`${AVATAR_DIR}/Analise_Laviss/avatar.png`, 'analise_l', 'analise_l') + await setEmail('analise_l', 'analise@example.org') + await setProfileField('analise_l', 'organisation', 'Charity Events Foundation') + await setProfileField('analise_l', 'role', 'Board Secretary') +} diff --git a/playwright/types.d.ts b/playwright/types.d.ts new file mode 100644 index 00000000000..b5a3c8b9e2c --- /dev/null +++ b/playwright/types.d.ts @@ -0,0 +1,5 @@ +// Type stubs for untyped packages used in global teardown. +declare module 'pngquant-bin' { + const bin: string + export default bin +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000000..86e24243008 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "types": ["node"] + }, + "include": [ + "playwright/**/*.ts", + "playwright.config.ts" + ] +}