From a6971657128ed01f8c0f923138bba5a8b970cf0b Mon Sep 17 00:00:00 2001 From: Gabriel Bruno Date: Mon, 23 Sep 2024 16:36:25 -0400 Subject: [PATCH] Add Playwright to the transition repo, and run a few simple tests on github. Fix: #1065 --- .env.example | 5 + .github/workflows/transition.yml | 44 +++- .gitignore | 7 +- README.md | 33 +++ package.json | 2 +- packages/chaire-lib-frontend/.eslintignore | 5 +- packages/chaire-lib-frontend/jest.config.js | 3 +- packages/chaire-lib-frontend/package.json | 3 +- .../playwright-example.config.ts | 78 +++++++ .../ui-tests/loginTestHelpers.ts | 29 +++ .../ui-tests/test-left-menu.spec.ts | 39 ++++ .../ui-tests/testHelpers.ts | 200 ++++++++++++++++++ yarn.lock | 54 +++-- 13 files changed, 467 insertions(+), 35 deletions(-) create mode 100644 packages/chaire-lib-frontend/playwright-example.config.ts create mode 100644 packages/chaire-lib-frontend/ui-tests/loginTestHelpers.ts create mode 100644 packages/chaire-lib-frontend/ui-tests/test-left-menu.spec.ts create mode 100644 packages/chaire-lib-frontend/ui-tests/testHelpers.ts diff --git a/.env.example b/.env.example index c8636e165..a447a87ea 100644 --- a/.env.example +++ b/.env.example @@ -70,3 +70,8 @@ MAIL_TRANSPORT_SMTP_AUTH_PWD=password # From email MAIL_FROM_ADDRESS=example@example.com + +#Parameters used to login to a test account in the playwright tests +PLAYWRIGHT_TEST_USER=testUser +PLAYWRIGHT_TEST_EMAIL=user@test.ts +PLAYWRIGHT_TEST_PASSWORD=testPassword \ No newline at end of file diff --git a/.github/workflows/transition.yml b/.github/workflows/transition.yml index bcf196401..fc4101c0a 100644 --- a/.github/workflows/transition.yml +++ b/.github/workflows/transition.yml @@ -12,11 +12,32 @@ on: jobs: build-and-test: runs-on: ubuntu-latest + services: + postgres: + image: postgis/postgis + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + POSTGRES_USER: testuser + POSTGRES_PASSWORD: testpassword + POSTGRES_DB: testdb strategy: matrix: node-version: [18.x, 20.x] env: PROJECT_CONFIG: ${{ github.workspace }}/examples/config.js + PG_CONNECTION_STRING_PREFIX: postgres://testuser:testpassword@localhost:5432/ + PG_DATABASE_PRODUCTION: testdb + PG_DATABASE_DEVELOPMENT: testdb + CI: true ## This is to make sure that the tests run in CI mode + PLAYWRIGHT_TEST_USER: testUser + PLAYWRIGHT_TEST_EMAIL: user@test.ts + PLAYWRIGHT_TEST_PASSWORD: testPassword steps: - uses: actions/checkout@v4 - name: copy env file @@ -33,8 +54,29 @@ jobs: run: yarn build:prod - name: Unit Test run: yarn test - - name: UI Test + # Following configure the automated UI tests + - name: Create DB + run: yarn setup && yarn migrate + env: + NODE_ENV: production + - name: Get Playwright config + run: cp packages/chaire-lib-frontend/playwright-example.config.ts packages/chaire-lib-frontend/playwright.config.ts + - name: Create test user + run: yarn create-user --username $PLAYWRIGHT_TEST_USER --email $PLAYWRIGHT_TEST_EMAIL --password $PLAYWRIGHT_TEST_PASSWORD --admin + - name: Start application + run: yarn start & + env: + NODE_ENV: production + - name: Run tests run: yarn test:ui + - name: Archive UI Test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-results-${{matrix.node-version}} # This is to make sure that the results are stored in a unique name + path: packages/chaire-lib-frontend/test-results + retention-days: 2 + # End of automated UI tests code-lint: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 039f32310..43264ef2b 100644 --- a/.gitignore +++ b/.gitignore @@ -88,4 +88,9 @@ tests/test profilingData* #example apps -examples/runtime \ No newline at end of file +examples/runtime + +# playwright test output +**/playwright-report/ +**/test-results/ +**/playwright.config.ts \ No newline at end of file diff --git a/README.md b/README.md index bb4ca1e52..3724617a3 100644 --- a/README.md +++ b/README.md @@ -163,6 +163,39 @@ For example, to run yarn test in the chaire-lib backend: You can also run the app this way with: `docker run -a STDOUT -it -v "${PWD}:/home/project" -w=/home/project/ testtransition yarn start` +### UI testing with Playwright + +To execute UI tests with Playwright, you first have to create an account that will be used for logging in to Transition during the tests. This only needs to be done once: +``` +yarn create-user --username testUser --email user@test.ts --password testPassword --admin +``` + +Next, configure Playwright by copying the example config file and select the browser to test in: +``` +cp packages/chaire-lib-frontend/playwright-example.config.ts packages/chaire-lib-frontend/playwright.config.ts +``` + +Next, install browser dependencies to correctly execute the tests. If it is not done, an arror message should tell you the command when attempting to run the test. It is possible to install each browser separaly with the following command, for example `firefox`: +``` +npx playwright install --with-deps firefox +``` + +Now that Playwright is configured, you need to start the application as you would to run it normally: +``` +yarn build:dev or yarn build:prod +yarn start +``` + +Then, to run the tests: +``` +yarn test:ui +``` + +You can also use this command to open a graphic interface that allows you to run tests individually and gives more info: +``` +yarn test:ui --ui +``` + ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md) diff --git a/package.json b/package.json index 3bdeb073c..353747207 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "test": "yarn workspaces run test", "test:unit": "yarn workspaces run test:unit", "test:sequential": "yarn workspaces run test:sequential", - "test:ui": "yarn workspaces run test:ui", + "test:ui": "yarn workspace chaire-lib-frontend run test:ui", "lint": "yarn workspaces run lint", "format": "yarn workspaces run format", "list-tasks": "yarn workspace transition-backend run list-tasks", diff --git a/packages/chaire-lib-frontend/.eslintignore b/packages/chaire-lib-frontend/.eslintignore index 33636c10f..7177dc335 100644 --- a/packages/chaire-lib-frontend/.eslintignore +++ b/packages/chaire-lib-frontend/.eslintignore @@ -1,4 +1,7 @@ lib/ node_modules/ **/__tests__ -jestSetup.ts \ No newline at end of file +jestSetup.ts +ui-tests/ +playwright-example.config.ts +playwright.config.ts \ No newline at end of file diff --git a/packages/chaire-lib-frontend/jest.config.js b/packages/chaire-lib-frontend/jest.config.js index df253f188..ceb35e130 100644 --- a/packages/chaire-lib-frontend/jest.config.js +++ b/packages/chaire-lib-frontend/jest.config.js @@ -15,5 +15,6 @@ module.exports = { './jestSetup.ts' ], testEnvironment: 'jsdom', - snapshotSerializers: ['enzyme-to-json/serializer'] + snapshotSerializers: ['enzyme-to-json/serializer'], + modulePathIgnorePatterns: ["spec.ts"], }; diff --git a/packages/chaire-lib-frontend/package.json b/packages/chaire-lib-frontend/package.json index 302d254eb..33feb6757 100644 --- a/packages/chaire-lib-frontend/package.json +++ b/packages/chaire-lib-frontend/package.json @@ -15,7 +15,7 @@ "test": "cross-env NODE_ENV=test jest --config=jest.config.js", "test:unit": "cross-env NODE_ENV=test jest --config=jest.config.js", "test:sequential": "echo 'cross-env NODE_ENV=test jest --config=jest.sequential.config.js --runInBand'", - "test:ui": "echo 'cross-env NODE_ENV=test jest --config=jest.ui.config.js'", + "test:ui": "LOCALE_DIR=$(pwd)/locales npx playwright test", "lint": "eslint .", "format": "prettier-eslint $PWD/'src/**/*.{ts,tsx}' --write" }, @@ -69,6 +69,7 @@ "typescript": "^4.9.4" }, "devDependencies": { + "@playwright/test": "^1.47.2", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^10.4.7", "@types/enzyme": "^3.10.8", diff --git a/packages/chaire-lib-frontend/playwright-example.config.ts b/packages/chaire-lib-frontend/playwright-example.config.ts new file mode 100644 index 000000000..aa7558b57 --- /dev/null +++ b/packages/chaire-lib-frontend/playwright-example.config.ts @@ -0,0 +1,78 @@ +import dotenv from 'dotenv'; +import path from 'path'; +import { defineConfig, devices } from '@playwright/test'; + +dotenv.config({ path: path.resolve(__dirname, '../../.env') }); + +export default defineConfig({ + testDir: './ui-tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + // Each test is given 15 seconds. + timeout: 15000, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:8080', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + screenshot: 'only-on-failure' + }, + + /* Configure projects for major browsers */ + projects: [ + // { + // name: 'chromium', + // use: { ...devices['Desktop Chrome'] }, + // }, + { + name: 'Google Chrome', + use: { ...devices['Desktop Chrome'], channel: 'chrome' } + } + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/packages/chaire-lib-frontend/ui-tests/loginTestHelpers.ts b/packages/chaire-lib-frontend/ui-tests/loginTestHelpers.ts new file mode 100644 index 000000000..b1dc28061 --- /dev/null +++ b/packages/chaire-lib-frontend/ui-tests/loginTestHelpers.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2024, Polytechnique Montreal and contributors + * + * This file is licensed under the MIT License. + * License text available at https://opensource.org/licenses/MIT + */ + +import * as testHelpers from './testHelpers'; + +/** + * Test the login page, change to the right language, and login to the test account. + * @param {Object} options - The options for the test. + * @param {string} options.title - The title of the page. + */ +export const startAndLoginAnonymously = ({ + context, + title +}: { title: string} & testHelpers.CommonTestParameters) => { + testHelpers.hasTitleTest({ context, title }); + testHelpers.hasUrlTest({ context, expectedUrl: '/login' }); + testHelpers.isLanguageTest({ context, expectedLanguage: 'fr' }); + testHelpers.switchLanguageTest({ context, languageToSwitch: 'en' }); + testHelpers.isLanguageTest({ context, expectedLanguage: 'en' }); + testHelpers.loginTest({ context }); +}; + +export const logout = ({ context }: testHelpers.CommonTestParameters) => { + testHelpers.logoutTest({ context }); +}; diff --git a/packages/chaire-lib-frontend/ui-tests/test-left-menu.spec.ts b/packages/chaire-lib-frontend/ui-tests/test-left-menu.spec.ts new file mode 100644 index 000000000..e3be50dec --- /dev/null +++ b/packages/chaire-lib-frontend/ui-tests/test-left-menu.spec.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2024, Polytechnique Montreal and contributors + * + * This file is licensed under the MIT License. + * License text available at https://opensource.org/licenses/MIT + */ + +import { test } from '@playwright/test'; +import * as testHelpers from './testHelpers'; +import * as loginTestHelpers from './loginTestHelpers'; + +const context = { + page: null as any, + title: '', + widgetTestCounters: {} +}; + +// Configure the tests to run in serial mode (one after the other) +test.describe.configure({ mode: 'serial' }); + +test.beforeAll(async ({ browser }) => { + context.page = await testHelpers.initializeTestPage(browser); +}); + +loginTestHelpers.startAndLoginAnonymously({ context, title: 'Transition' }); + +// Click all the sections in the left menu and check that the right panel becomes the correct one. +testHelpers.clickLeftMenuTest({ context, section: 'nodes' }); +testHelpers.clickLeftMenuTest({ context, section: 'services' }); +testHelpers.clickLeftMenuTest({ context, section: 'scenarios' }); +testHelpers.clickLeftMenuTest({ context, section: 'routing' }); +testHelpers.clickLeftMenuTest({ context, section: 'accessibilityMap' }); +testHelpers.clickLeftMenuTest({ context, section: 'batchCalculation' }); +testHelpers.clickLeftMenuTest({ context, section: 'gtfsImport' }); +testHelpers.clickLeftMenuTest({ context, section: 'gtfsExport' }); +testHelpers.clickLeftMenuTest({ context, section: 'preferences' }); +testHelpers.clickLeftMenuTest({ context, section: 'agencies' }); + +loginTestHelpers.logout({ context }); diff --git a/packages/chaire-lib-frontend/ui-tests/testHelpers.ts b/packages/chaire-lib-frontend/ui-tests/testHelpers.ts new file mode 100644 index 000000000..b9f238ccd --- /dev/null +++ b/packages/chaire-lib-frontend/ui-tests/testHelpers.ts @@ -0,0 +1,200 @@ +/* + * Copyright 2024, Polytechnique Montreal and contributors + * + * This file is licensed under the MIT License. + * License text available at https://opensource.org/licenses/MIT + */ + +import moment from 'moment'; +import { test, expect, Page, Browser, Locator } from '@playwright/test'; +import { env } from 'process'; + + +// Types for the tests +export type CommonTestParameters = { + context: { + // The main test page + page: Page; + // Store a counter for test names, to avoid duplicate test names. We have many objects to test and they may result in identical test names. + widgetTestCounters: { [testKey: string]: number }; + }; +}; +type Value = string; +type StringOrBoolean = string | boolean; +type Text = string; +type Url = string; +type Title = string; +type Path = string; +type Email = string; +type LeftMenuSections = 'agencies' | 'nodes' | 'services' | 'scenarios' | 'routing' | 'accessibilityMap' | 'batchCalculation' | 'gtfsImport' | 'gtfsExport' | 'preferences'; +type HasTitleTest = (params: { title: Title } & CommonTestParameters) => void; +type IsLanguageTest = (params: { expectedLanguage: "en" | "fr" } & CommonTestParameters) => void; +type SwitchLanguageTest = (params: { languageToSwitch: "en" | "fr" } & CommonTestParameters) => void; +type HasUrlTest = (params: { expectedUrl: Url } & CommonTestParameters) => void; +type LoginTest = (params: CommonTestParameters) => void; +type LogoutTest = (params: CommonTestParameters) => void; +type LeftMenuTest = (params: { section: LeftMenuSections } & CommonTestParameters) => void; + + +/** + * Open the browser before all the tests and go to the login page + * + * @param {Browser} browser - The test browser object + * @param {Object} options - The options for the test. + * @param {{ [param: string]: string} } options.urlSearchParams - Additional + * parameters to add to the URL as query string question. + * @param {boolean} options.ignoreHTTPSErrors - Whether to ignore HTTPS errors. + * These can happen if running the tests on a remote server with HTTPs (for + * example test instances) + */ +export const initializeTestPage = async ( + browser: Browser, + options: { urlSearchParams?: { [param: string]: string }, ignoreHTTPSErrors?: boolean } = {} +): Promise => { + const context = await browser.newContext({ ignoreHTTPSErrors: options.ignoreHTTPSErrors === true }); + const page = await context.newPage(); + + const baseUrlString = test.info().project.use.baseURL; + if (typeof baseUrlString === 'string' && options.urlSearchParams) { + // Add the search params to the base URL + const baseURL = new URL(baseUrlString); + Object.keys(options.urlSearchParams).forEach((param) => { + baseURL.searchParams.append(param, options.urlSearchParams![param]); + }); + await page.goto(baseURL.toString()); + } else { + // Go to home page + await page.goto('/'); + } + + return page; +}; + +/** + * Test that the current page has a specific title. + * @param {Object} options - The options for the test. + * @param {string} options.title - The title of the page. + */ +export const hasTitleTest: HasTitleTest = ({ context, title }) => { + test(`Has title ${title}`, async () => { + await expect(context.page).toHaveTitle(title); + }); +}; + +/** + * Test that the current page has a specific url. + * @param {Object} options - The options for the test. + * @param {string} options.expectedUrl - The url of the page. + */ +export const hasUrlTest: HasUrlTest = ({ context, expectedUrl }) => { + test(`Current page has the url ${expectedUrl}`, async () => { + await expect(context.page).toHaveURL(expectedUrl); + }); +}; + +/** + * Test that the language is what we expect. + * @param {Object} options - The options for the test. + * @param {string} options.expectedLanguage - The language we expect. Can be either 'en' or 'fr'. + */ +export const isLanguageTest: IsLanguageTest = ({ context, expectedLanguage }) => { + test(`The page is in ${expectedLanguage === 'fr' ? 'French' : 'English'}`, async () => { + const language = await context.page.locator("//html").getAttribute("lang"); + expect(language).toBe(expectedLanguage); + const languageButton = context.page.getByRole('button', { name: (expectedLanguage === 'fr' ? 'English' : 'Français') }); + await expect(languageButton).toBeVisible(); + }); +}; + +/** + * Switch to a language by clicking the button in the top right. + * @param {Object} options - The options for the test. + * @param {string} options.languageToSwitch - The language to switch to. Can be either 'en' or 'fr'. + */ +export const switchLanguageTest: SwitchLanguageTest = ({ context, languageToSwitch }) => { + test('Switch to the other language', async () => { + const languageButton = context.page.getByRole('button', { name: (languageToSwitch === 'fr' ? 'Français' : 'English') }); + await languageButton.click(); + }); +}; + +/** + * Login to the test account, and verify we end up on the right page. + * @param {Object} options - The options for the test. + */ +export const loginTest: LoginTest = ({ context }) => { + test(`Login to the test account`, async () => { + const userNameField = context.page.locator("id=usernameOrEmail"); + await userNameField.fill(process.env.PLAYWRIGHT_TEST_USER as string); + const passwordField = context.page.locator("id=password"); + await passwordField.fill(process.env.PLAYWRIGHT_TEST_PASSWORD as string); + const loginButton = context.page.getByRole('button', { name: 'Login' }); + await loginButton.click(); + //await expect(context.page).toHaveURL(/\/#10\//); //Check that /#10/ is present in the url after logging in + const logoutButton = context.page.getByRole('button', { name: 'Logout' }); + await expect(logoutButton).toBeVisible(); + const userButton = context.page.getByRole('button', { name: process.env.PLAYWRIGHT_TEST_USER }); + await expect(userButton).toBeVisible(); + }); +}; + +/** + * Logout, and verify we end up back on the login page. + * @param {Object} options - The options for the test. + */ +export const logoutTest: LogoutTest = ({ context }) => { + test('Logout from survey', async () => { + const logoutButton = context.page.getByRole('button', { name: 'Logout' }); + await logoutButton.click(); + await expect(context.page).toHaveURL('/login'); + }); +}; + +/** + * Click on one of the sections on the left menu, and check that the right panel is the correct one. + * @param {Object} options - The options for the test. + * @param {string} options.section - The section we click. + */ +export const clickLeftMenuTest: LeftMenuTest = ({ context, section }) => { + test(`Click the ${section} section of the left menu`, async () => { + const leftMenu = context.page.locator("//nav[@id='tr__left-menu']/ul[@class='tr__left-menu-container']"); + const sectionButton = leftMenu.locator(`//li/button[@data-section='${section}']`); + await sectionButton.click(); + const rightPanel = context.page.locator("//section[@id='tr__right-panel']/div[@class='tr__right-panel-inner']"); + const rightPanelTitle = await rightPanel.locator("//h3").nth(0).textContent(); + let expectedRightPanelTitle; + switch(section) { + case 'agencies': + expectedRightPanelTitle = 'Agencies'; + break; + case 'nodes': + expectedRightPanelTitle = 'Stop nodes'; + break; + case 'services': + expectedRightPanelTitle = 'Services'; + break; + case 'scenarios': + expectedRightPanelTitle = 'Scenarios'; + break; + case 'routing': + expectedRightPanelTitle = 'Routing'; + break; + case 'accessibilityMap': + expectedRightPanelTitle = 'Accessibility map'; + break; + case 'batchCalculation': + expectedRightPanelTitle = 'Calculation jobs'; + break; + case 'gtfsImport': + expectedRightPanelTitle = 'Import from a GTFS feed'; + break; + case 'gtfsExport': + expectedRightPanelTitle = 'Export as a GTFS feed'; + break; + case 'preferences': + expectedRightPanelTitle = 'Preferences'; + break; + } + expect(rightPanelTitle?.trim()).toBe(expectedRightPanelTitle); + }); +}; diff --git a/yarn.lock b/yarn.lock index f1187fa38..582a80e6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1569,6 +1569,13 @@ "@types/geojson" "^7946.0.7" type-fest "^1.2.1" +"@playwright/test@^1.47.2": + version "1.47.2" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.47.2.tgz#dbe7051336bfc5cc599954214f9111181dbc7475" + integrity sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ== + dependencies: + playwright "1.47.2" + "@popperjs/core@^2.9.2": version "2.11.5" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64" @@ -6967,7 +6974,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@^2.3.2, fsevents@~2.3.2: +fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -10773,6 +10780,20 @@ pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +playwright-core@1.47.2: + version "1.47.2" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.47.2.tgz#7858da9377fa32a08be46ba47d7523dbd9460a4e" + integrity sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ== + +playwright@1.47.2: + version "1.47.2" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.47.2.tgz#155688aa06491ee21fb3e7555b748b525f86eb20" + integrity sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA== + dependencies: + playwright-core "1.47.2" + optionalDependencies: + fsevents "2.3.2" + point-in-polygon-hao@^0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/point-in-polygon-hao/-/point-in-polygon-hao-0.0.7.tgz#4ecc4d62f64a7cfd5b9ca9c080a347af3fb5adf9" @@ -12606,7 +12627,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -12633,15 +12654,6 @@ string-width@^4.1.0, string-width@^4.2.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -12773,7 +12785,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -12801,13 +12813,6 @@ strip-ansi@^6.0.0: dependencies: ansi-regex "^5.0.0" -strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -13902,7 +13907,7 @@ workerpool@^6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -13929,15 +13934,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"