diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..1222c5a --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,103 @@ +name: E2E Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + TURBO_API: 'http://127.0.0.1:9080' + TURBO_TOKEN: 'turbo-token' + TURBO_TEAM: ${{ github.repository_owner }} + +jobs: + run_e2e_tests: + runs-on: ubuntu-latest + steps: + - name: ๐Ÿ“ฅ Checkout repo + uses: actions/checkout@v4 + + - name: ๐Ÿ“ Copy test environment variables + run: cp example.env .env + + - name: ๐Ÿ› ๏ธ Setup node + uses: actions/setup-node@v4 + with: + node-version: 18 + + - name: ๐Ÿ’พ Cache dependencies + id: cache-dependencies + uses: actions/cache@v4 + with: + path: '**/node_modules' + key: ${{ runner.os }}-${{ hashFiles('**/yarn.lock') }} + + - name: ๐Ÿ“ฆ Install dependencies + if: steps.cache-dependencies.outputs.cache-hit != 'true' + run: yarn install --immutable + + - name: ๐ŸŽญ Get installed Playwright version + id: playwright-version + run: echo "PLAYWRIGHT_VERSION=$(grep '@playwright/test@' yarn.lock | sed -n 's/.*npm:\([^":]*\).*/\1/p' | head -n 1)" >> $GITHUB_ENV + + - name: ๐Ÿ’พ Cache Playwright binaries + id: playwright-cache + uses: actions/cache@v4 + with: + path: | + ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} + + - name: ๐ŸŽญ Install Playwright Browsers + run: npx playwright install chromium --with-deps + if: steps.playwright-cache.outputs.cache-hit != 'true' + + - name: ๐Ÿš€ Setup local cache server for Turborepo + uses: felixmosh/turborepo-gh-artifacts@v3 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + server-token: ${{ env.TURBO_TOKEN }} + + - name: ๐Ÿ—๏ธ Build apps + run: yarn turbo run build --color --concurrency=5 + + - name: ๐Ÿš€ Run dev server + run: bash e2e/support/github/run-e2e-docker-env.sh + + - name: โณ Wait for OpenMRS instance to start + run: while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' http://localhost:8080/openmrs/login.htm)" != "200" ]]; do sleep 10; done + + - name: ๐Ÿงช Run E2E tests + run: yarn test-e2e + + - name: ๐Ÿ›‘ Stop dev server + if: '!cancelled()' + run: docker stop $(docker ps -a -q) + + - name: ๐Ÿ“ค Upload report + uses: actions/upload-artifact@v4 + if: '!cancelled()' + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 + overwrite: true + + - name: ๐Ÿ“ Capture Server Logs + if: always() + uses: jwalton/gh-docker-logs@v2 + with: + dest: './logs' + + - name: ๐Ÿ“ค Upload Logs as Artifact + uses: actions/upload-artifact@v4 + if: always() + with: + name: server-logs + path: './logs' + retention-days: 2 + overwrite: true + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5cb9af9..592bc3d 100644 --- a/.gitignore +++ b/.gitignore @@ -75,3 +75,7 @@ dist/ !.yarn/versions .turbo +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/README.md b/README.md index 23d21bd..a984cfb 100644 --- a/README.md +++ b/README.md @@ -56,3 +56,11 @@ yarn start --importmap "https://spa-modules.nyc3.digitaloceanspaces.com/import-m ``` To see more options run `npx openmrs --help` + +## To run end-to-end tests, run: + +```bash +yarn test-e2e +``` + +Read the [e2e testing guide](https://openmrs.atlassian.net/wiki/spaces/docs/pages/150962731/Testing+Frontend+Modules+O3) to learn more about End-to-End tests in this project. diff --git a/e2e/core/global-setup.ts b/e2e/core/global-setup.ts new file mode 100644 index 0000000..7489749 --- /dev/null +++ b/e2e/core/global-setup.ts @@ -0,0 +1,32 @@ +import { request } from '@playwright/test'; +import * as dotenv from 'dotenv'; + +dotenv.config(); + +/** + * This configuration is to reuse the signed-in state in the tests + * by log in only once using the API and then skip the log in step for all the tests. + * + * https://playwright.dev/docs/auth#reuse-signed-in-state + */ + +async function globalSetup() { + const requestContext = await request.newContext(); + const token = Buffer.from(`${process.env.E2E_USER_ADMIN_USERNAME}:${process.env.E2E_USER_ADMIN_PASSWORD}`).toString( + 'base64', + ); + await requestContext.post(`${process.env.E2E_BASE_URL}/ws/rest/v1/session`, { + data: { + sessionLocation: process.env.E2E_LOGIN_DEFAULT_LOCATION_UUID, + locale: 'en', + }, + headers: { + Accept: 'application/json', + Authorization: `Basic ${token}`, + }, + }); + await requestContext.storageState({ path: 'e2e/storageState.json' }); + await requestContext.dispose(); +} + +export default globalSetup; diff --git a/e2e/core/index.ts b/e2e/core/index.ts new file mode 100644 index 0000000..eade442 --- /dev/null +++ b/e2e/core/index.ts @@ -0,0 +1 @@ +export * from './test'; \ No newline at end of file diff --git a/e2e/core/test.ts b/e2e/core/test.ts new file mode 100644 index 0000000..dd3e40b --- /dev/null +++ b/e2e/core/test.ts @@ -0,0 +1,20 @@ +import { APIRequestContext, Page, test as base } from '@playwright/test'; +import { api } from '../fixtures'; + +// This file sets up our custom test harness using the custom fixtures. +// See https://playwright.dev/docs/test-fixtures#creating-a-fixture for details. +// If a spec intends to use one of the custom fixtures, the special `test` function +// exported from this file must be used instead of the default `test` function +// provided by playwright. + +export interface CustomTestFixtures { + loginAsAdmin: Page; +} + +export interface CustomWorkerFixtures { + api: APIRequestContext; +} + +export const test = base.extend({ + api: [api, { scope: 'worker' }], +}); diff --git a/e2e/fixtures/api.ts b/e2e/fixtures/api.ts new file mode 100644 index 0000000..1f6f803 --- /dev/null +++ b/e2e/fixtures/api.ts @@ -0,0 +1,26 @@ +import { APIRequestContext, PlaywrightWorkerArgs, WorkerFixture } from '@playwright/test'; + +/** + * A fixture which initializes an [`APIRequestContext`](https://playwright.dev/docs/api/class-apirequestcontext) + * that is bound to the configured OpenMRS API server. The context is automatically authenticated + * using the configured admin account. + * + * Use the request context like this: + * ```ts + * test('your test', async ({ api }) => { + * const res = await api.get('patient/1234'); + * await expect(res.ok()).toBeTruthy(); + * }); + * ``` + */ +export const api: WorkerFixture = async ({ playwright }, use) => { + const ctx = await playwright.request.newContext({ + baseURL: `${process.env.E2E_BASE_URL}/ws/rest/v1/`, + httpCredentials: { + username: process.env.E2E_USER_ADMIN_USERNAME, + password: process.env.E2E_USER_ADMIN_PASSWORD, + }, + }); + + await use(ctx); +}; \ No newline at end of file diff --git a/e2e/fixtures/index.ts b/e2e/fixtures/index.ts new file mode 100644 index 0000000..308f5ae --- /dev/null +++ b/e2e/fixtures/index.ts @@ -0,0 +1 @@ +export * from './api'; \ No newline at end of file diff --git a/e2e/pages/app-loads.ts b/e2e/pages/app-loads.ts new file mode 100644 index 0000000..557b42f --- /dev/null +++ b/e2e/pages/app-loads.ts @@ -0,0 +1,9 @@ +import { Page } from '@playwright/test'; + +export class FastDataEntryPage { + constructor(readonly page: Page) {} + + async goto() { + await this.page.goto(`fast-data-entry`); + } +} diff --git a/e2e/pages/index.ts b/e2e/pages/index.ts new file mode 100644 index 0000000..10a0c5f --- /dev/null +++ b/e2e/pages/index.ts @@ -0,0 +1 @@ +export * from './app-loads'; diff --git a/e2e/specs/app-loads.ts b/e2e/specs/app-loads.ts new file mode 100644 index 0000000..be01ee7 --- /dev/null +++ b/e2e/specs/app-loads.ts @@ -0,0 +1,10 @@ +import { test, expect } from '@playwright/test'; +import { FastDataEntryPage } from '../pages'; + +// This test is a sample E2E test. You can delete it. + +test('app-loads', async ({ page }) => { + const fastDataEntryPage = new FastDataEntryPage(page); + await fastDataEntryPage.goto(); + await expect(page).toHaveTitle('Fast Data Entry'); +}); diff --git a/e2e/storageState.json b/e2e/storageState.json new file mode 100644 index 0000000..1bc63e6 --- /dev/null +++ b/e2e/storageState.json @@ -0,0 +1,15 @@ +{ + "cookies": [ + { + "name": "JSESSIONID", + "value": "E0E268EE278C5E782DC3C15BD3AC4B61", + "domain": "localhost", + "path": "/openmrs", + "expires": -1, + "httpOnly": true, + "secure": false, + "sameSite": "Lax" + } + ], + "origins": [] +} \ No newline at end of file diff --git a/e2e/support/bamboo/docker-compose.yml b/e2e/support/bamboo/docker-compose.yml new file mode 100644 index 0000000..ce895c4 --- /dev/null +++ b/e2e/support/bamboo/docker-compose.yml @@ -0,0 +1,56 @@ +# This docker compose file is used to create a dockerized environment when running E2E tests on Bamboo. +version: '3.7' + +services: + playwright: + build: + context: . + dockerfile: playwright.Dockerfile + args: + USER_ID: ${USER_ID} + GROUP_ID: ${GROUP_ID} + container_name: patient-chart-e2e-tests-container + working_dir: /app + command: sh /app/e2e/support/bamboo/e2e-test-runner.sh + volumes: + - ../../../:/app + networks: + - test + + gateway: + image: openmrs/openmrs-reference-application-3-gateway:${TAG:-nightly} + depends_on: + - frontend + - backend + ports: + - '80:80' + networks: + - test + + frontend: + image: openmrs/openmrs-reference-application-3-frontend:${TAG:-nightly} + environment: + SPA_PATH: /openmrs/spa + API_URL: /openmrs + SPA_CONFIG_URLS: + healthcheck: + test: ['CMD', 'curl', '-f', 'http://localhost/'] + timeout: 5s + depends_on: + - backend + networks: + - test + + backend: + image: openmrs/openmrs-reference-application-3-backend:nightly-with-data + depends_on: + - db + networks: + - test + db: + image: openmrs/openmrs-reference-application-3-db:nightly-with-data + networks: + - test + +networks: + test: diff --git a/e2e/support/bamboo/e2e-test-runner.sh b/e2e/support/bamboo/e2e-test-runner.sh new file mode 100644 index 0000000..3d2df2e --- /dev/null +++ b/e2e/support/bamboo/e2e-test-runner.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +export E2E_BASE_URL=http://gateway/openmrs +export CI=true + +while [ "$(curl -s -o /dev/null -w ''%{http_code}'' http://gateway/openmrs/login.htm)" != "200" ]; do + echo "Waiting for the backend to be up..." + sleep 10 +done + +cp example.env .env + +yarn test-e2e diff --git a/e2e/support/bamboo/playwright.Dockerfile b/e2e/support/bamboo/playwright.Dockerfile new file mode 100644 index 0000000..291c3d5 --- /dev/null +++ b/e2e/support/bamboo/playwright.Dockerfile @@ -0,0 +1,13 @@ +# The image version should match the Playwright version specified in the package.json file +FROM mcr.microsoft.com/playwright:v1.48.2-jammy + +ARG USER_ID +ARG GROUP_ID + +RUN if ! getent group $GROUP_ID > /dev/null; then \ + groupadd -g $GROUP_ID myusergroup; \ + fi + +RUN useradd -u $USER_ID -g $GROUP_ID -m playwrightuser + +USER playwrightuser diff --git a/e2e/support/github/Dockerfile b/e2e/support/github/Dockerfile new file mode 100644 index 0000000..6823603 --- /dev/null +++ b/e2e/support/github/Dockerfile @@ -0,0 +1,34 @@ +# syntax=docker/dockerfile:1.3 +FROM --platform=$BUILDPLATFORM node:18-alpine as dev + +ARG APP_SHELL_VERSION=next + +RUN mkdir -p /app +WORKDIR /app + +COPY . . + +RUN npm_config_legacy_peer_deps=true npm install -g openmrs@${APP_SHELL_VERSION:-next} +ARG CACHE_BUST +RUN npm_config_legacy_peer_deps=true openmrs assemble --manifest --mode config --config spa-assemble-config.json --target ./spa + +FROM --platform=$BUILDPLATFORM openmrs/openmrs-reference-application-3-frontend:nightly as frontend +FROM nginx:1.23-alpine + +RUN apk update && \ + apk upgrade && \ + # add more utils for sponge to support our startup script + apk add --no-cache moreutils + +# clear any default files installed by nginx +RUN rm -rf /usr/share/nginx/html/* + +COPY --from=frontend /etc/nginx/nginx.conf /etc/nginx/nginx.conf +# this assumes that NOTHING in the framework is in a subdirectory +COPY --from=frontend /usr/share/nginx/html/* /usr/share/nginx/html/ +COPY --from=frontend /usr/local/bin/startup.sh /usr/local/bin/startup.sh +RUN chmod +x /usr/local/bin/startup.sh + +COPY --from=dev /app/spa/ /usr/share/nginx/html/ + +CMD ["/usr/local/bin/startup.sh"] diff --git a/e2e/support/github/docker-compose.yml b/e2e/support/github/docker-compose.yml new file mode 100644 index 0000000..b95aaf1 --- /dev/null +++ b/e2e/support/github/docker-compose.yml @@ -0,0 +1,24 @@ +# This docker compose file is used to create a backend environment for the e2e.yml workflow. +version: '3.7' + +services: + gateway: + image: openmrs/openmrs-reference-application-3-gateway:${TAG:-nightly} + ports: + - '8080:80' + + frontend: + build: + context: . + environment: + SPA_PATH: /openmrs/spa + API_URL: /openmrs + + backend: + image: openmrs/openmrs-reference-application-3-backend:nightly-with-data + depends_on: + - db + + # MariaDB + db: + image: openmrs/openmrs-reference-application-3-db:nightly-with-data diff --git a/e2e/support/github/run-e2e-docker-env.sh b/e2e/support/github/run-e2e-docker-env.sh new file mode 100644 index 0000000..dce5703 --- /dev/null +++ b/e2e/support/github/run-e2e-docker-env.sh @@ -0,0 +1,49 @@ +!/usr/bin/env bash -eu + +# get the dir containing the script +script_dir=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +# create a temporary working directory +working_dir=$(mktemp -d "${TMPDIR:-/tmp/}openmrs-e2e-frontends.XXXXXXXXXX") +# get a list of all the apps in this workspace +apps=$(yarn workspaces list --json | jq -r 'if ((.location == ".") or (.location | test("-app") | not)) then halt else .name end') +# this array will hold all of the packed app names +app_names=() + +echo "Creating packed archives of apps..." +# for each app +for app in $apps +do + # @openmrs/esm-whatever -> _openmrs_esm_whatever + app_name=$(echo "$app" | tr '[:punct:]' '_'); + # add to our array + app_names+=("$app_name.tgz"); + # run yarn pack for our app and add it to the working directory + yarn workspace "$app" pack -o "$working_dir/$app_name.tgz" >/dev/null; +done; +echo "Created packed app archives" + +echo "Creating dynamic spa-assemble-config.json..." +# dynamically assemble our list of frontend modules, prepending the login app and +# primary navigation apps; apps will all be in the /app directory of the Docker +# container +jq -n \ + --arg apps "$apps" \ + --arg app_names "$(echo ${app_names[@]})" \ + '{"@openmrs/esm-primary-navigation-app": "next", "@openmrs/esm-home-app": "next"} + ( + ($apps | split("\n")) as $apps | ($app_names | split(" ") | map("/app/" + .)) as $app_files + | [$apps, $app_files] + | transpose + | map({"key": .[0], "value": .[1]}) + | from_entries + )' | jq '{"frontendModules": .}' > "$working_dir/spa-assemble-config.json" +echo "Created dynamic spa-assemble-config.json" + +echo "Copying Docker configuration..." +cp "$script_dir/Dockerfile" "$working_dir/Dockerfile" +cp "$script_dir/docker-compose.yml" "$working_dir/docker-compose.yml" + +cd $working_dir +echo "Starting Docker containers..." +# CACHE_BUST to ensure the assemble step is always run +docker compose build --build-arg CACHE_BUST=$(date +%s) frontend +docker compose up -d diff --git a/example.env b/example.env new file mode 100644 index 0000000..a307ef8 --- /dev/null +++ b/example.env @@ -0,0 +1,6 @@ +# This is an example environment file for configuring dynamic values. +E2E_BASE_URL=http://localhost:8080/openmrs/ +E2E_USER_ADMIN_USERNAME=admin +E2E_USER_ADMIN_PASSWORD=Admin123 +E2E_LOGIN_DEFAULT_LOCATION_UUID=44c3efb0-2583-4c80-a79e-1f756a03c0a1 +# The above location UUID is for the "Outpatient Clinic" location in the reference application \ No newline at end of file diff --git a/jest.config.json b/jest.config.json index 913d2e4..a877944 100644 --- a/jest.config.json +++ b/jest.config.json @@ -1,22 +1,31 @@ { + "clearMocks": true, + "collectCoverageFrom": [ + "**/src/**/*.component.tsx", + "!**/node_modules/**", + "!**/src/declarations.d.ts", + "!**/e2e/**" + ], "transform": { - "^.+\\.tsx?$": "@swc/jest" + "^.+\\.tsx?$": ["@swc/jest"] }, "transformIgnorePatterns": ["/node_modules/(?!@openmrs)"], "moduleNameMapper": { - "\\.(s?css)$": "identity-obj-proxy", "@openmrs/esm-framework": "@openmrs/esm-framework/mock", + "\\.(s?css)$": "identity-obj-proxy", "^lodash-es/(.*)$": "lodash/$1", - "^uuid$": "/node_modules/uuid/dist/index.js", + "^lodash-es$": "lodash", "^dexie$": "/node_modules/dexie", - "lodash-es": "lodash" + "^react-i18next$": "/__mocks__/react-i18next.js" }, - "setupFilesAfterEnv": [ - "/src/setup-tests.ts" - ], + "setupFilesAfterEnv": ["/src/setup-tests.ts"], "testEnvironment": "jsdom", "testEnvironmentOptions": { "url": "http://localhost/" - } + }, + "testPathIgnorePatterns": [ + "/node_modules/", + "/e2e/" + ] } diff --git a/package.json b/package.json index d61522b..6822473 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,8 @@ "verify": "turbo lint typescript test", "coverage": "yarn test -- --coverage ", "postinstall": "husky install", - "extract-translations": "i18next 'src/**/*.tsx' --config ./tools/i18next-parser.config.js" + "extract-translations": "i18next 'src/**/*.tsx' --config ./tools/i18next-parser.config.js", + "test-e2e": "playwright test" }, "browserslist": [ "extends browserslist-config-openmrs" @@ -51,6 +52,7 @@ "devDependencies": { "@carbon/react": "1.71.0", "@openmrs/esm-framework": "next", + "@playwright/test": "^1.49.1", "@swc-node/loader": "^1.3.7", "@swc/core": "^1.3.84", "@swc/jest": "^0.2.29", @@ -93,8 +95,10 @@ "webpack-cli": "^5.1.4" }, "dependencies": { + "dotenv": "^16.4.7", "i18next": "^21.10.0", "i18next-parser": "^6.6.0", + "playwright": "^1.49.1", "react-hook-form": "^7.34.2", "turbo": "^2.3.3", "uuid": "^9.0.1" diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..3628dbd --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,32 @@ +import { devices, PlaywrightTestConfig } from '@playwright/test'; +import * as dotenv from 'dotenv'; +dotenv.config(); + +// See https://playwright.dev/docs/test-configuration. +const config: PlaywrightTestConfig = { + testDir: './e2e/specs', + timeout: 3 * 60 * 1000, + expect: { + timeout: 40 * 1000, + }, + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: 0, + reporter: process.env.CI ? [['junit', { outputFile: 'results.xml' }], ['html']] : [['html']], + globalSetup: require.resolve('./e2e/core/global-setup'), + use: { + baseURL: `${process.env.E2E_BASE_URL}/spa/`, + storageState: 'e2e/storageState.json', + video: 'retain-on-failure', + }, + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + ], +}; + +export default config; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 38d4704..f13dbf6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3606,6 +3606,7 @@ __metadata: dependencies: "@carbon/react": "npm:1.71.0" "@openmrs/esm-framework": "npm:next" + "@playwright/test": "npm:^1.49.1" "@swc-node/loader": "npm:^1.3.7" "@swc/core": "npm:^1.3.84" "@swc/jest": "npm:^0.2.29" @@ -3622,6 +3623,7 @@ __metadata: "@typescript-eslint/parser": "npm:^6.7.0" concurrently: "npm:^6.5.1" css-loader: "npm:^6.8.1" + dotenv: "npm:^16.4.7" eslint: "npm:^8.49.0" eslint-config-react-app: "npm:^7.0.1" eslint-plugin-import: "npm:^2.31.0" @@ -3635,6 +3637,7 @@ __metadata: jest-environment-jsdom: "npm:^28.1.3" lodash-es: "npm:^4.17.21" openmrs: "npm:next" + playwright: "npm:^1.49.1" prettier: "npm:^2.8.8" pretty-quick: "npm:^3.1.3" react: "npm:^18.2.0" @@ -3891,6 +3894,17 @@ __metadata: languageName: node linkType: hard +"@playwright/test@npm:^1.49.1": + version: 1.49.1 + resolution: "@playwright/test@npm:1.49.1" + dependencies: + playwright: "npm:1.49.1" + bin: + playwright: cli.js + checksum: 10/bb0d5eda58ee0b5bbca732d2aa57782fadf420d101e08e16d5760179459c667907bd8d224ee3d6f43f3088378e377ef63d32ed605fec37605debf217c3efe8da + languageName: node + linkType: hard + "@pnpm/config.env-replace@npm:^1.1.0": version: 1.1.0 resolution: "@pnpm/config.env-replace@npm:1.1.0" @@ -10084,6 +10098,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:^16.4.7": + version: 16.4.7 + resolution: "dotenv@npm:16.4.7" + checksum: 10/f13bfe97db88f0df4ec505eeffb8925ec51f2d56a3d0b6d916964d8b4af494e6fb1633ba5d09089b552e77ab2a25de58d70259b2c5ed45ec148221835fc99a0c + languageName: node + linkType: hard + "downshift@npm:8.1.0": version: 8.1.0 resolution: "downshift@npm:8.1.0" @@ -11499,7 +11520,7 @@ __metadata: languageName: node linkType: hard -"fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": +"fsevents@npm:2.3.2, fsevents@npm:^2.3.2, fsevents@npm:~2.3.2": version: 2.3.2 resolution: "fsevents@npm:2.3.2" dependencies: @@ -11509,7 +11530,7 @@ __metadata: languageName: node linkType: hard -"fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": +"fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A^2.3.2#optional!builtin, fsevents@patch:fsevents@npm%3A~2.3.2#optional!builtin": version: 2.3.2 resolution: "fsevents@patch:fsevents@npm%3A2.3.2#optional!builtin::version=2.3.2&hash=df0bf1" dependencies: @@ -15786,6 +15807,30 @@ __metadata: languageName: node linkType: hard +"playwright-core@npm:1.49.1": + version: 1.49.1 + resolution: "playwright-core@npm:1.49.1" + bin: + playwright-core: cli.js + checksum: 10/baa39a53024ec7744708410f2b952ac3aa2e1a6d311dabfa303523712848eba142fce5c20f1b2ed2a66fbd9a415d22ea8642b0f70423360aaebd4b41c47d364e + languageName: node + linkType: hard + +"playwright@npm:1.49.1, playwright@npm:^1.49.1": + version: 1.49.1 + resolution: "playwright@npm:1.49.1" + dependencies: + fsevents: "npm:2.3.2" + playwright-core: "npm:1.49.1" + dependenciesMeta: + fsevents: + optional: true + bin: + playwright: cli.js + checksum: 10/49fb063f4a107b8090f66d2d351ebd51fbb66843a8f95a161fa0c0e0b5156515961e75cc10f4249f61b9d2af51f762dda505c62b096d8f61cd47d1ff73ab39d2 + languageName: node + linkType: hard + "pngjs@npm:^3.0.0, pngjs@npm:^3.3.3": version: 3.4.0 resolution: "pngjs@npm:3.4.0"