diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index d23172fc6..123d54f09 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -78,3 +78,39 @@ jobs: - name: Test run: yarn test + + playwright_test: + timeout-minutes: 60 + needs: prep + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Use Node.js ${{ env.NODE }} + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE }} + - name: Cache node_modules + uses: actions/cache@v2 + id: cache-node-modules + with: + path: | + node_modules + .veda/ui/node_modules + key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package.json') }} + - name: Install dependencies + run: yarn + - name: Install UI + run: ./.veda/setup + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: MAPBOX_TOKEN="${{secrets.MAPBOX_TOKEN}}" yarn test:e2e + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 16647576d..1fcc568bd 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,15 @@ dist Makefile .hypothesis + +################################################ +# Playwright Test Outputs +# +# Files generated by running Playwright tests +################################################ + +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/e2e/playwrightTestData.json \ No newline at end of file diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 000000000..075171b81 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,35 @@ +# Playwright E2E Testing + +This testing suite is for End to End testing the VEDA website via the UI using Playwright. It works by serving a local version of the site using yarn serve and performing UI checks against that locally hosted site. The suite is designed to generate a `playwrightTestData.json` that contains the list of all Catalog and Story names. This is done by parsing the `name` field in the `*.mdx` files of the `/datasets` and `/stories` directories. + +## Running the test suite + +The test suite can be run via the `yarn test:e2e` script. There is a `prtest:e2e` script that will generate a new playwrightTestData.json before beginning the actual playwright test run. This allows for new stories or catalogs to be added without updating the test suite. + +## Directory Structure + +The end to end tests are organized in the `/e2e` directory. The tests have been written following a [Page Object Model](https://martinfowler.com/bliki/PageObject.html) pattern. +Supporting files within the repo are in the following structure: + +```text +/e2e + │ + │─── README.md + │─── playwright.config.ts - imports our global setup, defines preferred browsers, & number of retries + │─── generateTestData.js - parses mdx files and creates a json based on their metadata + └─── /pages + │ └─── basePage.ts - imports all seeded data and PageObjects into our `test` object. + │ │ + │ └─── [PAGENAME]Page.ts - The page objects. UI elements interacted with on a page are defined once to keep tests DRY and minimize test changes if layout changes. + └─── tests - our actual tests +``` + +## Updating Tests + +If the layout of a page changes, then the tests may no longer be able to interact with locators. These locators are defined in the Page Objects defined in `/e2e/pages`. The Playwright framework provides multiple ways to choose elements to interact with. The recomended ones are defined in the [Playwright documentation](https://playwright.dev/docs/locators#quick-guide). + +Any new pages will need to have new page objects created and then imported into the `basePage.ts` file following th format of existing pages. This allows all tests to reference the page. + +## Output + +Playwright will generate an html report with test results. This report will show all tests that were run, and will allow a user to view the results of any failed tests. diff --git a/e2e/generateTestData.js b/e2e/generateTestData.js new file mode 100644 index 000000000..d9c05eb5c --- /dev/null +++ b/e2e/generateTestData.js @@ -0,0 +1,42 @@ +const fs = require('fs'); +const path = require('path'); +const matter = require('gray-matter'); +const fg = require('fast-glob'); + +const catalogPaths = fg.globSync('**/datasets/*.mdx'); +const storyPaths = fg.globSync('**/stories/*.mdx'); +const catalogNames = []; +const datasetIds = []; +const storyNames = []; + +for (const catalog of catalogPaths) { + const catalogData = matter.read(catalog).data; + catalogNames.push(catalogData['name']); + datasetIds.push(catalogData['id']); +} + +for (const story of storyPaths) { + const storyData = matter.read(story).data; + storyNames.push(storyData['name']); +} + +const testDataJson = { + catalogs: catalogNames, + datasetIds: datasetIds, + stories: storyNames +}; + +fs.writeFile( + path.join(__dirname, 'playwrightTestData.json'), + JSON.stringify(testDataJson), + (err) => { + if (err) { + // eslint-disable-next-line no-console + console.error(err); + throw err; + } else { + // eslint-disable-next-line no-console + console.info('new test data file generated'); + } + } +); \ No newline at end of file diff --git a/e2e/pages/aboutPage.ts b/e2e/pages/aboutPage.ts new file mode 100644 index 000000000..a9e84eac3 --- /dev/null +++ b/e2e/pages/aboutPage.ts @@ -0,0 +1,11 @@ +import { Locator, Page } from '@playwright/test'; + +export default class AboutPage { + readonly page: Page; + readonly mainContent: Locator; + + constructor(page: Page) { + this.page = page; + this.mainContent = this.page.getByRole('main'); + } +} \ No newline at end of file diff --git a/e2e/pages/basePage.ts b/e2e/pages/basePage.ts new file mode 100644 index 000000000..af0612dbe --- /dev/null +++ b/e2e/pages/basePage.ts @@ -0,0 +1,47 @@ +import { test as base } from '@playwright/test'; +import AboutPage from './aboutPage'; +import CatalogPage from './catalogPage'; +import DatasetPage from './datasetPage'; +import ExplorePage from './explorePage'; +import FooterComponent from './footerComponent'; +import HeaderComponent from './headerComponent'; +import HomePage from './homePage'; +import StoryPage from './storyPage'; + +export const test = base.extend<{ + aboutPage: AboutPage; + catalogPage: CatalogPage; + datasetPage: DatasetPage; + explorePage: ExplorePage; + footerComponent: FooterComponent; + headerComponent: HeaderComponent; + homePage: HomePage; + storyPage: StoryPage +}> ({ + aboutPage: async ({page}, use) => { + await use(new AboutPage(page)); + }, + catalogPage: async ({page}, use) => { + await use(new CatalogPage(page)); + }, + datasetPage: async ({page}, use) => { + await use(new DatasetPage(page)); + }, + explorePage: async ({page}, use) => { + await use(new ExplorePage(page)); + }, + footerComponent: async ({page}, use) => { + await use(new FooterComponent(page)); + }, + headerComponent: async ({page}, use) => { + await use(new HeaderComponent(page)); + }, + homePage: async ({page}, use) => { + await use(new HomePage(page)); + }, + storyPage: async ({page}, use) => { + await use(new StoryPage(page)); + }, +}); + +export const expect = test.expect; \ No newline at end of file diff --git a/e2e/pages/catalogPage.ts b/e2e/pages/catalogPage.ts new file mode 100644 index 000000000..64025f5df --- /dev/null +++ b/e2e/pages/catalogPage.ts @@ -0,0 +1,14 @@ +import { Locator, Page } from '@playwright/test'; + +export default class CatalogPage { + readonly page: Page; + readonly mainContent: Locator; + readonly header: Locator; + + + constructor(page: Page) { + this.page = page; + this.mainContent = this.page.getByRole('main'); + this.header = this.mainContent.getByRole('heading', {level: 1}) + } +} \ No newline at end of file diff --git a/e2e/pages/datasetPage.ts b/e2e/pages/datasetPage.ts new file mode 100644 index 000000000..75386bca9 --- /dev/null +++ b/e2e/pages/datasetPage.ts @@ -0,0 +1,18 @@ +import { Locator, Page } from '@playwright/test'; + +export default class DatasetPage { + readonly page: Page; + readonly mainContent: Locator; + readonly header: Locator; + readonly exploreDataButton: Locator; + readonly analyzeDataButton: Locator; + + + constructor(page: Page) { + this.page = page; + this.mainContent = this.page.getByRole('main'); + this.header = this.mainContent.getByRole('heading', { level: 1 }) + this.exploreDataButton = this.page.getByRole('link', {name: /explore data/i} ); + this.analyzeDataButton = this.page.getByRole('button', {name: /analyze data/i} ); + } +} \ No newline at end of file diff --git a/e2e/pages/explorePage.ts b/e2e/pages/explorePage.ts new file mode 100644 index 000000000..379a3c2bc --- /dev/null +++ b/e2e/pages/explorePage.ts @@ -0,0 +1,11 @@ +import { Locator, Page } from '@playwright/test'; + +export default class ExplorePage { + readonly page: Page; + readonly exploreDataLink: Locator; + + constructor(page: Page) { + this.page = page; + this.exploreDataLink = this.page.getByRole('link', { name: /Explore data/i }) + } +} \ No newline at end of file diff --git a/e2e/pages/footerComponent.ts b/e2e/pages/footerComponent.ts new file mode 100644 index 000000000..90d5bdf8d --- /dev/null +++ b/e2e/pages/footerComponent.ts @@ -0,0 +1,13 @@ +import { Locator, Page } from '@playwright/test'; + +export default class FooterComponent { + readonly page: Page; + readonly footer: Locator; + readonly partners: Locator; + + constructor(page: Page) { + this.page = page; + this.footer = this.page.locator('footer'); + this.partners = this.footer.locator('div'); + } +} \ No newline at end of file diff --git a/e2e/pages/headerComponent.ts b/e2e/pages/headerComponent.ts new file mode 100644 index 000000000..96808702a --- /dev/null +++ b/e2e/pages/headerComponent.ts @@ -0,0 +1,23 @@ +import { Locator, Page } from '@playwright/test'; + +export default class HeaderComponent { + readonly page: Page; + readonly header: Locator; + readonly welcomeLink: Locator; + readonly dataCatalogLink: Locator; + readonly analysisLink: Locator; + readonly dataInsightsLink: Locator; + readonly aboutLink: Locator; + readonly feedbackLink: Locator; + + constructor(page: Page) { + this.page = page; + this.header = this.page.getByRole('navigation'); + this.welcomeLink = this.header.getByRole('link', {name: /welcome/i}); + this.dataCatalogLink = this.header.getByRole('link', {name: / data catalog/i}); + this.analysisLink = this.header.getByRole('link', {name: /analysis/i}); + this.dataInsightsLink = this.header.getByRole('link', {name: /data insights/i}); + this.aboutLink = this.header.getByRole('link', {name: /about/i}); + this.feedbackLink = this.header.getByRole('link', {name: /feedback/i}); + } +} \ No newline at end of file diff --git a/e2e/pages/homePage.ts b/e2e/pages/homePage.ts new file mode 100644 index 000000000..5fa13895e --- /dev/null +++ b/e2e/pages/homePage.ts @@ -0,0 +1,14 @@ +import { Locator, Page } from '@playwright/test'; + +export default class HomePage { + readonly page: Page; + readonly mainContent: Locator; + readonly headingContainer: Locator; + + + constructor(page: Page) { + this.page = page; + this.mainContent = this.page.getByRole('main'); + this.headingContainer = this.mainContent.locator('div').filter({ hasText: 'U.S. Greenhouse Gas CenterUniting Data and Technology to Empower Tomorrow\'s' }).nth(2) + } +} \ No newline at end of file diff --git a/e2e/pages/storyPage.ts b/e2e/pages/storyPage.ts new file mode 100644 index 000000000..358b2caa2 --- /dev/null +++ b/e2e/pages/storyPage.ts @@ -0,0 +1,14 @@ +import { Locator, Page } from '@playwright/test'; + +export default class StoryPage { + readonly page: Page; + readonly mainContent: Locator; + readonly header: Locator; + + + constructor(page: Page) { + this.page = page; + this.mainContent = this.page.getByRole('main'); + this.header = this.mainContent.getByRole('heading', {level: 1}) + } +} \ No newline at end of file diff --git a/e2e/tests/catalog.spec.ts b/e2e/tests/catalog.spec.ts new file mode 100644 index 000000000..c9e7c4b33 --- /dev/null +++ b/e2e/tests/catalog.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from '../pages/basePage'; + +const catalogs = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['catalogs']; + +test('load catalogs on /data-catalog route', async ({ + page, + catalogPage, + }) => { + let pageErrorCalled = false; + // Log all uncaught errors to the terminal + page.on('pageerror', exception => { + console.log(`Uncaught exception: "${exception}"`); + pageErrorCalled = true; + }); + + await page.goto('/data-catalog'); + await expect(catalogPage.header, `catalog page should load`).toHaveText(/data catalog/i); + + for (const item of catalogs) { + const catalogCard = catalogPage.mainContent.getByRole('article').getByRole('heading', { level: 3, name: item, exact: true}).last(); + await catalogCard.scrollIntoViewIfNeeded(); + await expect(catalogCard, `${item} catalog card should load`).toBeVisible(); + }; + + expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false) + +}); \ No newline at end of file diff --git a/e2e/tests/catalogRouting.spec.ts b/e2e/tests/catalogRouting.spec.ts new file mode 100644 index 000000000..8161311d5 --- /dev/null +++ b/e2e/tests/catalogRouting.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from '../pages/basePage'; + +const catalogs = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['catalogs']; + +test.describe('catalog card routing', () => { + for (const item of catalogs) { + test(`${item} routes to dataset details page`, async({ + page, + catalogPage, + datasetPage, + }) => { + let pageErrorCalled = false; + // Log all uncaught errors to the terminal + page.on('pageerror', exception => { + console.log(`Uncaught exception: "${exception}"`); + pageErrorCalled = true; + }); + + await page.goto('/data-catalog'); + await expect(catalogPage.header, `catalog page should load`).toHaveText(/data catalog/i); + + const catalogCard = catalogPage.mainContent.getByRole('article').getByRole('heading', { level: 3, name: item, exact: true}).first(); + await catalogCard.scrollIntoViewIfNeeded(); + await catalogCard.click({force: true}); + + await expect(datasetPage.header.filter({ hasText: item}), `${item} page should load`).toBeVisible(); + + // scroll page to bottom + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + + expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false) + }) + } + +}); \ No newline at end of file diff --git a/e2e/tests/exploreDatasets.spec.ts b/e2e/tests/exploreDatasets.spec.ts new file mode 100644 index 000000000..c69437aa7 --- /dev/null +++ b/e2e/tests/exploreDatasets.spec.ts @@ -0,0 +1,37 @@ +import fs from 'fs'; +import { test, expect } from '../pages/basePage'; + +const datasetIds = JSON.parse(fs.readFileSync('e2e/playwrightTestData.json', 'utf8')).datasetIds; +const datasetName = JSON.parse(fs.readFileSync('e2e/playwrightTestData.json', 'utf8')).catalogNames; +test.describe('explore dataset', () => { + for (const dataset of datasetIds) { + test(`${dataset} explore page functions`, async({ + page, + explorePage, + }) => { + let pageErrorCalled = false; + // Log all uncaught errors to the terminal to be visible in trace + page.on('pageerror', exception => { + console.log(`Uncaught exception: "${JSON.stringify(exception)}"`); + pageErrorCalled = true; + }); + + //mosaic isn't hit on all datasets + // const collectionsResponsePromise = page.waitForResponse(response => + // response.url().includes('collections') && response.status() === 200 + // ); + + await page.goto(`data-catalog/${dataset}`); + await expect(explorePage.exploreDataLink).toBeVisible(); + + // const mosaicResponse = await collectionsResponsePromise; + // expect(mosaicResponse.ok(), 'mapbox request should be successful').toBeTruthy(); + + // scroll page to bottom + await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + + expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false); + }); + } + +}); \ No newline at end of file diff --git a/e2e/tests/stories.spec.ts b/e2e/tests/stories.spec.ts new file mode 100644 index 000000000..4b3adb0fe --- /dev/null +++ b/e2e/tests/stories.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '../pages/basePage'; + +const stories = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['stories']; + +test('load stories on /stories route', async ({ + page, + storyPage, + }) => { + + + + let pageErrorCalled = false; + // Log all uncaught errors to the terminal + page.on('pageerror', exception => { + console.log(`Uncaught exception: "${exception}"`); + pageErrorCalled = true; + }); + + await page.goto('/stories'); + await expect(storyPage.header, `data stories page should load`).toHaveText(/data stories/i); + + for (const item of stories) { + const storiesCard = storyPage.mainContent.getByRole('article').getByRole('heading', { level: 3, name: item, exact: true}).first(); + await storiesCard.scrollIntoViewIfNeeded(); + await expect(storiesCard, `${item} story card should load`).toBeVisible(); + }; + + expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false) + +}); \ No newline at end of file diff --git a/e2e/tests/storiesRouting.spec.ts b/e2e/tests/storiesRouting.spec.ts new file mode 100644 index 000000000..8336f16cf --- /dev/null +++ b/e2e/tests/storiesRouting.spec.ts @@ -0,0 +1,30 @@ +import { test, expect } from '../pages/basePage'; + +const stories = JSON.parse(require('fs').readFileSync('e2e/playwrightTestData.json', 'utf8'))['stories']; + +test.describe('stories card routing', () => { + for (const item of stories) { + test(`${item} routes to dataset details page`, async({ + page, + storyPage, + datasetPage, + }) => { + let pageErrorCalled = false; + // Log all uncaught errors to the terminal + page.on('pageerror', exception => { + console.log(`Uncaught exception: "${exception}"`); + pageErrorCalled = true; + }); + + await page.goto('/stories'); + await expect(storyPage.header, `stories page should load`).toHaveText(/data stories/i); + + const storyCard = storyPage.mainContent.getByRole('article').getByRole('heading', { level: 3, name: item, exact: true}).last(); + await storyCard.scrollIntoViewIfNeeded(); + await storyCard.click({force: true}); + await expect(datasetPage.header.filter({ hasText: item}), `${item} page should load`).toBeVisible(); + expect(pageErrorCalled, 'no javascript exceptions thrown on page').toBe(false) + }) + } + +}); \ No newline at end of file diff --git a/package.json b/package.json index af6bd1759..cfb4ea407 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,9 @@ "stage": "NODE_ENV=staging .veda/veda", "clean": ".veda/veda clean", "local-cms": "npx netlify-cms-proxy-server", - "test": "NODE_ENV=test .veda/veda test" + "test": "NODE_ENV=test .veda/veda test", + "pretest:e2e": "node e2e/generateTestData.js", + "test:e2e": "yarn playwright test" }, "targets": { "veda-app": { @@ -26,7 +28,11 @@ "devDependencies": { "@parcel/packager-raw-url": "2.7.0", "@parcel/transformer-webmanifest": "2.7.0", + "@playwright/test": "^1.41.1", + "@types/node": "^20.11.6", "dotenv": "^10.0.0", + "fast-glob": "^3.3.2", + "gray-matter": "^4.0.3", "netlify-cms-proxy-server": "^1.3.24" }, "alias": { diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 000000000..8e892f65c --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,60 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* 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, + // Global single test timeout + timeout: 300000, + // For expect calls + expect: { + timeout: 180000, + }, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : 3, + /* 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:9000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'retain-on-failure', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + // uncomment to also run tests in Firefox and webkit + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, + + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, + + ], + + /* Run your local dev server before starting the tests */ + webServer: { + timeout: 6 * 60 * 1000, + command: 'yarn serve', + url: 'http://localhost:9000', + reuseExistingServer: !process.env.CI, + stdout: 'pipe' + }, +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index c998a22d2..6ec4d6e7a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -149,6 +149,27 @@ resolved "https://registry.yarnpkg.com/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz#0f164b726869f71da3c594171df5ebc1c4b0a407" integrity sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ== +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + "@parcel/cache@2.7.0": version "2.7.0" resolved "https://registry.yarnpkg.com/@parcel/cache/-/cache-2.7.0.tgz#cc4b99685c7ff0fc20fbc321f4b6850d6e0c6811" @@ -313,6 +334,20 @@ chrome-trace-event "^1.0.2" nullthrows "^1.1.1" +"@playwright/test@^1.41.1": + version "1.41.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.41.1.tgz#6954139ed4a67999f1b17460aa3d184f4b334f18" + integrity sha512-9g8EWTjiQ9yFBXc6HjCWe41msLpxEX0KhmfmPl9RPLJdfzL4F0lg2BdJ91O9azFdl11y1pmpwdjBiSxvqc+btw== + dependencies: + playwright "1.41.1" + +"@types/node@^20.11.6": + version "20.11.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.6.tgz#6adf4241460e28be53836529c033a41985f85b6e" + integrity sha512-+EOokTnksGVgip2PbYbr3xnR7kZigh4LbybAfBAw5BpnQ+FqBYUsvCEjYd70IXKlbohQ64mzEYmMtlWUY8q//Q== + dependencies: + undici-types "~5.26.4" + accepts@~1.3.8: version "1.3.8" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" @@ -328,6 +363,13 @@ ansi-styles@^4.1.0: dependencies: color-convert "^2.0.1" +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + array-flatten@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" @@ -535,6 +577,11 @@ escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" @@ -577,6 +624,31 @@ express@^4.17.1: utils-merge "1.0.1" vary "~1.1.2" +extend-shallow@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f" + integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug== + dependencies: + is-extendable "^0.1.0" + +fast-glob@^3.3.2: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fastq@^1.6.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.0.tgz#ca5e1a90b5e68f97fc8b61330d5819b82f5fab03" + integrity sha512-zGygtijUMT7jnk3h26kUms3BkSDp4IfIKjmnqI2tvx6nuBfiF1UqOxbnLfzdv+apBy+53oaImsKtMw/xYbW+1w== + dependencies: + reusify "^1.0.4" + fecha@^4.2.0: version "4.2.3" resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd" @@ -617,6 +689,11 @@ fresh@0.5.2: resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== +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== + function-bind@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" @@ -631,6 +708,23 @@ get-intrinsic@^1.0.2: has "^1.0.3" has-symbols "^1.0.3" +glob-parent@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +gray-matter@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798" + integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q== + dependencies: + js-yaml "^3.13.1" + kind-of "^6.0.2" + section-matter "^1.0.0" + strip-bom-string "^1.0.0" + has-flag@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" @@ -681,12 +775,17 @@ is-arrayish@^0.3.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03" integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ== +is-extendable@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw== + is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== -is-glob@^4.0.3: +is-glob@^4.0.1, is-glob@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== @@ -703,11 +802,24 @@ is-stream@^2.0.0: resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +js-yaml@^3.13.1: + version "3.14.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" + integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + json5@^2.2.1: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== +kind-of@^6.0.0, kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + kuler@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3" @@ -752,12 +864,17 @@ merge-descriptors@1.0.1: resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromatch@^4.0.5: +micromatch@^4.0.4, micromatch@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== @@ -935,6 +1052,20 @@ picomatch@^2.3.1: resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +playwright-core@1.41.1: + version "1.41.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.41.1.tgz#9c152670010d9d6f970f34b68e3e935d3c487431" + integrity sha512-/KPO5DzXSMlxSX77wy+HihKGOunh3hqndhqeo/nMxfigiKzogn8kfL0ZBDu0L1RKgan5XHCPmn6zXd2NUJgjhg== + +playwright@1.41.1: + version "1.41.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.41.1.tgz#83325f34165840d019355c2a78a50f21ed9b9c85" + integrity sha512-gdZAWG97oUnbBdRL3GuBvX3nDDmUOuqzV/D24dytqlKt+eI5KbwusluZRGljx1YoJKZ2NRPaeWiFTeGZO7SosQ== + dependencies: + playwright-core "1.41.1" + optionalDependencies: + fsevents "2.3.2" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -950,6 +1081,11 @@ qs@6.11.0: dependencies: side-channel "^1.0.4" +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + range-parser@~1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" @@ -974,6 +1110,18 @@ readable-stream@^3.4.0, readable-stream@^3.6.0: string_decoder "^1.1.1" util-deprecate "^1.0.1" +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + safe-buffer@5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" @@ -994,6 +1142,14 @@ safe-stable-stringify@^2.3.1: resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== +section-matter@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/section-matter/-/section-matter-1.0.0.tgz#e9041953506780ec01d59f292a19c7b850b84167" + integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA== + dependencies: + extend-shallow "^2.0.1" + kind-of "^6.0.0" + semver@^5.7.1: version "5.7.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" @@ -1058,6 +1214,11 @@ simple-swizzle@^0.2.2: dependencies: is-arrayish "^0.3.1" +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== + stack-trace@0.0.x: version "0.0.10" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" @@ -1075,6 +1236,11 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +strip-bom-string@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/strip-bom-string/-/strip-bom-string-1.0.0.tgz#e5211e9224369fbb81d633a2f00044dc8cedad92" + integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g== + supports-color@^7.1.0: version "7.2.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" @@ -1117,6 +1283,11 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"