From 8fbb30c6f405206b242aab4c5b2b9d6700c0f2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E9=91=AB=E5=90=8C=E5=AD=A6?= Date: Sat, 14 Sep 2024 01:11:49 +0800 Subject: [PATCH] Add local e2e testing (#175) --- .github/workflows/e2e.yml | 35 ++++++++++++ .gitignore | 5 ++ e2e/content-css-module.spec.ts | 32 +++++++++++ e2e/content-extension-config.spec.ts | 55 +++++++++++++++++++ e2e/content-less.spec.ts | 32 +++++++++++ e2e/content-main-world.spec.ts | 32 +++++++++++ e2e/content-preact.spec.ts | 55 +++++++++++++++++++ e2e/content-react.spec.ts | 55 +++++++++++++++++++ e2e/content-sass-module.spec.ts | 32 +++++++++++ e2e/content-sass.spec.ts | 33 ++++++++++++ e2e/content-shadow-dom.spec.ts | 55 +++++++++++++++++++ e2e/content-tailwind.spec.ts | 55 +++++++++++++++++++ e2e/content-typescript.spec.ts | 32 +++++++++++ e2e/content-vue.spec.ts | 55 +++++++++++++++++++ e2e/content.spec.ts | 32 +++++++++++ e2e/extension-fixtures.ts | 62 +++++++++++++++++++++ e2e/new-less.spec.ts | 26 +++++++++ e2e/new-preact.spec.ts | 26 +++++++++ e2e/new-react-router.spec.ts | 26 +++++++++ e2e/new-react.spec.ts | 26 +++++++++ e2e/new-sass.spec.ts | 26 +++++++++ e2e/new-tailwind.spec.ts | 26 +++++++++ e2e/new-typescript.spec.ts | 26 +++++++++ e2e/new-vue.spec.ts | 26 +++++++++ e2e/new.spec.ts | 26 +++++++++ package.json | 2 + playwright.config.ts | 80 ++++++++++++++++++++++++++++ pnpm-lock.yaml | 38 +++++++++++++ 28 files changed, 1011 insertions(+) create mode 100644 .github/workflows/e2e.yml create mode 100644 e2e/content-css-module.spec.ts create mode 100644 e2e/content-extension-config.spec.ts create mode 100644 e2e/content-less.spec.ts create mode 100644 e2e/content-main-world.spec.ts create mode 100644 e2e/content-preact.spec.ts create mode 100644 e2e/content-react.spec.ts create mode 100644 e2e/content-sass-module.spec.ts create mode 100644 e2e/content-sass.spec.ts create mode 100644 e2e/content-shadow-dom.spec.ts create mode 100644 e2e/content-tailwind.spec.ts create mode 100644 e2e/content-typescript.spec.ts create mode 100644 e2e/content-vue.spec.ts create mode 100644 e2e/content.spec.ts create mode 100644 e2e/extension-fixtures.ts create mode 100644 e2e/new-less.spec.ts create mode 100644 e2e/new-preact.spec.ts create mode 100644 e2e/new-react-router.spec.ts create mode 100644 e2e/new-react.spec.ts create mode 100644 e2e/new-sass.spec.ts create mode 100644 e2e/new-tailwind.spec.ts create mode 100644 e2e/new-typescript.spec.ts create mode 100644 e2e/new-vue.spec.ts create mode 100644 e2e/new.spec.ts create mode 100644 playwright.config.ts diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..6425bd8b --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,35 @@ +name: e2e tests + +on: + push: + +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20] + steps: + - uses: actions/checkout@v4 + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: Install dependencies + run: pnpm install + - name: Run compiler + run: pnpm compile + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run e2e tests + run: pnpm test:e2e + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: e2e-report + path: e2e-report/ + retention-days: 15 diff --git a/.gitignore b/.gitignore index 03f77d2e..54d1a008 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,8 @@ __TEST__ /examples/**/yarn.lock /examples/**/pnpm-lock.yaml /examples/**/extension-env.d.ts +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/e2e-report/ \ No newline at end of file diff --git a/e2e/content-css-module.spec.ts b/e2e/content-css-module.spec.ts new file mode 100644 index 00000000..0fb89da3 --- /dev/null +++ b/e2e/content-css-module.spec.ts @@ -0,0 +1,32 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-css-module'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('body > div.content_script-box'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h1 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + await test.expect(h1).toHaveText('Change the background-color ⬇'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(51, 51, 51)'); +}) diff --git a/e2e/content-extension-config.spec.ts b/e2e/content-extension-config.spec.ts new file mode 100644 index 00000000..891f5731 --- /dev/null +++ b/e2e/content-extension-config.spec.ts @@ -0,0 +1,55 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-extension-config'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name extension-root', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('#extension-root'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h2 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + await test.expect(h2).toHaveText('This is a content script running React, TypeScript, and Tailwind.css'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h2.elementHandle()); + await test.expect(color).toEqual('rgb(255, 255, 255)'); +}) + +test('should load all images successfully', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const images = page.locator('#extension-root img'); + const imageElements = await images.all(); + + const results: boolean[] = []; + + for (const image of imageElements) { + const naturalWidth = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalWidth : 0; + }, await image.elementHandle()); + + const naturalHeight = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalHeight : 0; + }, await image.elementHandle()); + + const loadedSuccessfully = naturalWidth > 0 && naturalHeight > 0; + results.push(loadedSuccessfully); + } + + await test.expect(results.every(result => result)).toBeTruthy(); +}) \ No newline at end of file diff --git a/e2e/content-less.spec.ts b/e2e/content-less.spec.ts new file mode 100644 index 00000000..1954aa70 --- /dev/null +++ b/e2e/content-less.spec.ts @@ -0,0 +1,32 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-less'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('body > div.content_script-box'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h1 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + await test.expect(h1).toHaveText('Change the background-color ⬇'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(51, 51, 51)'); +}) diff --git a/e2e/content-main-world.spec.ts b/e2e/content-main-world.spec.ts new file mode 100644 index 00000000..20e2f20a --- /dev/null +++ b/e2e/content-main-world.spec.ts @@ -0,0 +1,32 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-main-world'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('body > div.content_script-box'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h1 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + await test.expect(h1).toHaveText('Main World'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(51, 51, 51)'); +}) diff --git a/e2e/content-preact.spec.ts b/e2e/content-preact.spec.ts new file mode 100644 index 00000000..e1eb7062 --- /dev/null +++ b/e2e/content-preact.spec.ts @@ -0,0 +1,55 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-preact'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name extension-root', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('#extension-root'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h2 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + await test.expect(h2).toHaveText('This is a content script running Preact, TypeScript, and Tailwind.css.'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h2.elementHandle()); + await test.expect(color).toEqual('rgb(255, 255, 255)'); +}) + +test('should load all images successfully', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const images = page.locator('#extension-root img'); + const imageElements = await images.all(); + + const results: boolean[] = []; + + for (const image of imageElements) { + const naturalWidth = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalWidth : 0; + }, await image.elementHandle()); + + const naturalHeight = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalHeight : 0; + }, await image.elementHandle()); + + const loadedSuccessfully = naturalWidth > 0 && naturalHeight > 0; + results.push(loadedSuccessfully); + } + + await test.expect(results.every(result => result)).toBeTruthy(); +}) \ No newline at end of file diff --git a/e2e/content-react.spec.ts b/e2e/content-react.spec.ts new file mode 100644 index 00000000..9f1918f4 --- /dev/null +++ b/e2e/content-react.spec.ts @@ -0,0 +1,55 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-react'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name extension-root', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('#extension-root'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h2 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + await test.expect(h2).toHaveText('This is a content script running React, TypeScript, and Tailwind.css'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h2.elementHandle()); + await test.expect(color).toEqual('rgb(255, 255, 255)'); +}) + +test('should load all images successfully', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const images = page.locator('#extension-root img'); + const imageElements = await images.all(); + + const results: boolean[] = []; + + for (const image of imageElements) { + const naturalWidth = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalWidth : 0; + }, await image.elementHandle()); + + const naturalHeight = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalHeight : 0; + }, await image.elementHandle()); + + const loadedSuccessfully = naturalWidth > 0 && naturalHeight > 0; + results.push(loadedSuccessfully); + } + + await test.expect(results.every(result => result)).toBeTruthy(); +}) \ No newline at end of file diff --git a/e2e/content-sass-module.spec.ts b/e2e/content-sass-module.spec.ts new file mode 100644 index 00000000..011372a1 --- /dev/null +++ b/e2e/content-sass-module.spec.ts @@ -0,0 +1,32 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-sass-module'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('body > div.content_script-box'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h1 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + await test.expect(h1).toHaveText('Change the background-color ⬇'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(51, 51, 51)'); +}) diff --git a/e2e/content-sass.spec.ts b/e2e/content-sass.spec.ts new file mode 100644 index 00000000..1af79ed7 --- /dev/null +++ b/e2e/content-sass.spec.ts @@ -0,0 +1,33 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-sass'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('body > div.content_script-box'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h1 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + await test.expect(h1).toHaveText('Change the background-color ⬇'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(51, 51, 51)'); +}) + diff --git a/e2e/content-shadow-dom.spec.ts b/e2e/content-shadow-dom.spec.ts new file mode 100644 index 00000000..15055440 --- /dev/null +++ b/e2e/content-shadow-dom.spec.ts @@ -0,0 +1,55 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-shadow-dom'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name extension-root', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('#extension-root'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h2 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + await test.expect(h2).toHaveText('This is a content script running Tailwind.css.'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h2.elementHandle()); + await test.expect(color).toEqual('rgb(255, 255, 255)'); +}) + +test('should load all images successfully', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const images = page.locator('#extension-root img'); + const imageElements = await images.all(); + + const results: boolean[] = []; + + for (const image of imageElements) { + const naturalWidth = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalWidth : 0; + }, await image.elementHandle()); + + const naturalHeight = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalHeight : 0; + }, await image.elementHandle()); + + const loadedSuccessfully = naturalWidth > 0 && naturalHeight > 0; + results.push(loadedSuccessfully); + } + + await test.expect(results.every(result => result)).toBeTruthy(); +}) \ No newline at end of file diff --git a/e2e/content-tailwind.spec.ts b/e2e/content-tailwind.spec.ts new file mode 100644 index 00000000..9dde8482 --- /dev/null +++ b/e2e/content-tailwind.spec.ts @@ -0,0 +1,55 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-tailwind'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name extension-root', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('#extension-root'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h2 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + await test.expect(h2).toHaveText('This is a content script running Tailwind.css.'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h2.elementHandle()); + await test.expect(color).toEqual('rgb(255, 255, 255)'); +}) + +test('should load all images successfully', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const images = page.locator('#extension-root img'); + const imageElements = await images.all(); + + const results: boolean[] = []; + + for (const image of imageElements) { + const naturalWidth = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalWidth : 0; + }, await image.elementHandle()); + + const naturalHeight = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalHeight : 0; + }, await image.elementHandle()); + + const loadedSuccessfully = naturalWidth > 0 && naturalHeight > 0; + results.push(loadedSuccessfully); + } + + await test.expect(results.every(result => result)).toBeTruthy(); +}) \ No newline at end of file diff --git a/e2e/content-typescript.spec.ts b/e2e/content-typescript.spec.ts new file mode 100644 index 00000000..646d57ec --- /dev/null +++ b/e2e/content-typescript.spec.ts @@ -0,0 +1,32 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-typescript'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('body > div.content_script-box'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h1 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + await test.expect(h1).toHaveText('Change the background-color ⬇'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(51, 51, 51)'); +}) diff --git a/e2e/content-vue.spec.ts b/e2e/content-vue.spec.ts new file mode 100644 index 00000000..333ba4ac --- /dev/null +++ b/e2e/content-vue.spec.ts @@ -0,0 +1,55 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content-vue'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name extension-root', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('#extension-root'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h2 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + await test.expect(h2).toHaveText('This is a content script running Vue, TypeScript, and Tailwind.css.'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h2 = page.locator('#extension-root h2'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h2.elementHandle()); + await test.expect(color).toEqual('rgb(255, 255, 255)'); +}) + +test('should load all images successfully', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const images = page.locator('#extension-root img'); + const imageElements = await images.all(); + + const results: boolean[] = []; + + for (const image of imageElements) { + const naturalWidth = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalWidth : 0; + }, await image.elementHandle()); + + const naturalHeight = await page.evaluate(img => { + return img ? (img as HTMLImageElement).naturalHeight : 0; + }, await image.elementHandle()); + + const loadedSuccessfully = naturalWidth > 0 && naturalHeight > 0; + results.push(loadedSuccessfully); + } + + await test.expect(results.every(result => result)).toBeTruthy(); +}) \ No newline at end of file diff --git a/e2e/content.spec.ts b/e2e/content.spec.ts new file mode 100644 index 00000000..261fe14f --- /dev/null +++ b/e2e/content.spec.ts @@ -0,0 +1,32 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/content'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const div = page.locator('body > div.content_script-box'); + await test.expect(div).toBeVisible(); +}) + +test('should exist an h1 element with specified content', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + await test.expect(h1).toHaveText('Change the background-color ⬇'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('https://extension.js.org/'); + const h1 = page.locator('body > div.content_script-box > h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(51, 51, 51)'); +}) diff --git a/e2e/extension-fixtures.ts b/e2e/extension-fixtures.ts new file mode 100644 index 00000000..33cd547a --- /dev/null +++ b/e2e/extension-fixtures.ts @@ -0,0 +1,62 @@ +import { test as base, chromium, type BrowserContext } from '@playwright/test'; + +export const extensionFixtures = (pathToExtension: string, headless: boolean) => { + return base.extend<{ + context: BrowserContext; + extensionId: string; + }>({ + context: async ({ }, use) => { + const context = await chromium.launchPersistentContext('', { + headless: false, + args: [ + headless ? `--headless=new` : '', + `--disable-extensions-except=${pathToExtension}`, + `--load-extension=${pathToExtension}`, + '--no-first-run', // Disable Chrome's native first run experience. + '--disable-client-side-phishing-detection', // Disables client-side phishing detection + '--disable-component-extensions-with-background-pages', // Disable some built-in extensions that aren't affected by '--disable-extensions' + '--disable-default-apps', // Disable installation of default apps + '--disable-features=InterestFeedContentSuggestions', // Disables the Discover feed on NTP + '--disable-features=Translate', // Disables Chrome translation, both the manual option and the popup prompt when a page with differing language is detected. + '--hide-scrollbars', // Hide scrollbars from screenshots. + '--mute-audio', // Mute any audio + '--no-default-browser-check', // Disable the default browser check, do not prompt to set it as such + '--no-first-run', // Skip first run wizards + '--ash-no-nudges', // Avoids blue bubble "user education" nudges (eg., "… give your browser a new look", Memory Saver) + '--disable-search-engine-choice-screen', // Disable the 2023+ search engine choice screen + '--disable-features=MediaRoute', // Avoid the startup dialog for `Do you want the application “Chromium.app” to accept incoming network connections?`. Also disables the Chrome Media Router which creates background networking activity to discover cast targets. A superset of disabling DialMediaRouteProvider. + '--use-mock-keychain', // Use mock keychain on Mac to prevent the blocking permissions dialog about "Chrome wants to use your confidential information stored in your keychain" + '--disable-background-networking', // Disable various background network services, including extension updating, safe browsing service, upgrade detector, translate, UMA + '--disable-breakpad', // Disable crashdump collection (reporting is already disabled in Chromium) + '--disable-component-update', // Don't update the browser 'components' listed at chrome://components/ + '--disable-domain-reliability', // Disables Domain Reliability Monitoring, which tracks whether the browser has difficulty contacting Google-owned sites and uploads reports to Google. + '--disable-features=AutofillServerCommunicatio', // Disables autofill server communication. This feature isn't disabled via other 'parent' flags. + '--disable-features=CertificateTransparencyComponentUpdate', + '--disable-sync', // Disable syncing to a Google account + '--disable-features=OptimizationHints', // Used for turning on Breakpad crash reporting in a debug environment where crash reporting is typically compiled but disabled. Disable the Chrome Optimization Guide and networking with its service API + '--disable-features=DialMediaRouteProvider', // A weaker form of disabling the MediaRouter feature. See that flag's details. + '--no-pings', // Don't send hyperlink auditing pings + '--enable-features=SidePanelUpdates', // Ensure the side panel is visible. This is used for testing the side panel feature. + ].filter(arg => !!arg), + }); + await use(context); + await context.close(); + }, + extensionId: async ({ context }, use) => { + /* + // for manifest v2: + let [background] = context.backgroundPages() + if (!background) + background = await context.waitForEvent('backgroundpage') + */ + + // for manifest v3: + let [background] = context.serviceWorkers(); + if (!background) + background = await context.waitForEvent('serviceworker'); + + const extensionId = background.url().split('/')[2]; + await use(extensionId); + }, + }); +} \ No newline at end of file diff --git a/e2e/new-less.spec.ts b/e2e/new-less.spec.ts new file mode 100644 index 00000000..d48b03ee --- /dev/null +++ b/e2e/new-less.spec.ts @@ -0,0 +1,26 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/new-less'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + await test.expect(h1).toHaveText('Welcome to your LESS Extension'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(74, 74, 74)'); +}) diff --git a/e2e/new-preact.spec.ts b/e2e/new-preact.spec.ts new file mode 100644 index 00000000..ce1a3564 --- /dev/null +++ b/e2e/new-preact.spec.ts @@ -0,0 +1,26 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/new-preact'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + await test.expect(h1).toHaveText('Welcome to your Preact Extension.'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(74, 74, 74)'); +}) diff --git a/e2e/new-react-router.spec.ts b/e2e/new-react-router.spec.ts new file mode 100644 index 00000000..d5593b20 --- /dev/null +++ b/e2e/new-react-router.spec.ts @@ -0,0 +1,26 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/new-react-router'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + await test.expect(h1).toHaveText('Welcome to your React Router DOM Extension.'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(74, 74, 74)'); +}) diff --git a/e2e/new-react.spec.ts b/e2e/new-react.spec.ts new file mode 100644 index 00000000..f0abfe08 --- /dev/null +++ b/e2e/new-react.spec.ts @@ -0,0 +1,26 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/new-react'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + await test.expect(h1).toHaveText('Welcome to your React Extension.'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(74, 74, 74)'); +}) diff --git a/e2e/new-sass.spec.ts b/e2e/new-sass.spec.ts new file mode 100644 index 00000000..067012ea --- /dev/null +++ b/e2e/new-sass.spec.ts @@ -0,0 +1,26 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/new-sass'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + await test.expect(h1).toHaveText('Welcome to your New Extension'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(74, 74, 74)'); +}) diff --git a/e2e/new-tailwind.spec.ts b/e2e/new-tailwind.spec.ts new file mode 100644 index 00000000..72e904f8 --- /dev/null +++ b/e2e/new-tailwind.spec.ts @@ -0,0 +1,26 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/new-tailwind'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h2 = page.locator('h2'); + await test.expect(h2).toHaveText('This is a new tab page running React and Tailwind.css.'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h2 = page.locator('h2'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h2.elementHandle()); + await test.expect(color).toEqual('rgb(255, 255, 255)'); +}) diff --git a/e2e/new-typescript.spec.ts b/e2e/new-typescript.spec.ts new file mode 100644 index 00000000..6efbee27 --- /dev/null +++ b/e2e/new-typescript.spec.ts @@ -0,0 +1,26 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/new-typescript'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + await test.expect(h1).toHaveText('Welcome to your TypeScript Extension.'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(74, 74, 74)'); +}) diff --git a/e2e/new-vue.spec.ts b/e2e/new-vue.spec.ts new file mode 100644 index 00000000..8fb24001 --- /dev/null +++ b/e2e/new-vue.spec.ts @@ -0,0 +1,26 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/new-vue'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + await test.expect(h1).toHaveText('Welcome to your Vue Extension.'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(74, 74, 74)'); +}) diff --git a/e2e/new.spec.ts b/e2e/new.spec.ts new file mode 100644 index 00000000..46434ce0 --- /dev/null +++ b/e2e/new.spec.ts @@ -0,0 +1,26 @@ +import path from 'path'; +import { execSync } from 'child_process'; +import { extensionFixtures } from './extension-fixtures'; + +const exampleDir = 'examples/new'; +const pathToExtension = path.join(__dirname, `../${exampleDir}/dist/chrome`); +const test = extensionFixtures(pathToExtension, true); + +test.beforeAll(async () => { + execSync(`pnpm extension build ${exampleDir}`, { cwd: path.join(__dirname, '..') }); +}) + +test('should exist an element with the class name content_script-box', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + await test.expect(h1).toHaveText('Welcome to your New Extension'); +}) + +test('should exist a default color value', async ({ page }) => { + await page.goto('chrome://newtab/'); + const h1 = page.locator('h1'); + const color = await page.evaluate((locator) => { + return window.getComputedStyle(locator!).getPropertyValue('color'); + }, await h1.elementHandle()); + await test.expect(color).toEqual('rgb(74, 74, 74)'); +}) diff --git a/package.json b/package.json index 9bc45a31..e97ed094 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "test:create": "dotenv -- turbo run test:create", "test:cli": "dotenv -- turbo run test:cli", "test:watch": "dotenv -- turbo run test:watch", + "test:e2e": "pnpm exec playwright test", "types": "pnpm tsc", "version-packages": "changeset version", "publish-packages": "turbo run compile lint test && changeset version && changeset publish", @@ -27,6 +28,7 @@ "devDependencies": { "@changesets/cli": "^2.27.1", "@eslint/js": "^9.6.0", + "@playwright/test": "^1.47.0", "@types/chrome": "^0.0.268", "@types/jest": "^29.5.12", "@types/node": "^20.14.12", diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 00000000..bc09703a --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,80 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * 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, + /* 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', { outputFolder: 'e2e-report' }] + ], + /* 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://127.0.0.1:3000', + + /* 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'] }, + }, + + // { + // 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/pnpm-lock.yaml b/pnpm-lock.yaml index 7d78075d..b698491d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,6 +21,9 @@ importers: '@eslint/js': specifier: ^9.6.0 version: 9.9.1 + '@playwright/test': + specifier: ^1.47.0 + version: 1.47.0 '@types/chrome': specifier: ^0.0.268 version: 0.0.268 @@ -2056,6 +2059,11 @@ packages: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} + '@playwright/test@1.47.0': + resolution: {integrity: sha512-SgAdlSwYVpToI4e/IH19IHHWvoijAYH5hu2MWSXptRypLSnzj51PcGD+rsOXFayde4P9ZLi+loXVwArg6IUkCA==} + engines: {node: '>=18'} + hasBin: true + '@pmmmwh/react-refresh-webpack-plugin@0.5.15': resolution: {integrity: sha512-LFWllMA55pzB9D34w/wXUCf8+c+IYKuJDgxiZ3qMhl64KRMBHYM1I3VdGaD2BV5FNPV2/S2596bppxHbv2ZydQ==} engines: {node: '>= 10.13'} @@ -3760,6 +3768,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -4850,6 +4863,16 @@ packages: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} + playwright-core@1.47.0: + resolution: {integrity: sha512-1DyHT8OqkcfCkYUD9zzUTfg7EfTd+6a8MkD/NWOvjo0u/SCNd5YmY/lJwFvUZOxJbWNds+ei7ic2+R/cRz/PDg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.47.0: + resolution: {integrity: sha512-jOWiRq2pdNAX/mwLiwFYnPHpEZ4rM+fRSQpRHwEwZlP2PUANvL3+aJOF/bvISMhFD30rqMxUB4RJx9aQbfh4Ww==} + engines: {node: '>=18'} + hasBin: true + postcss-attribute-case-insensitive@6.0.3: resolution: {integrity: sha512-KHkmCILThWBRtg+Jn1owTnHPnFit4OkqS+eKiGEOPIGke54DCeYGJ6r0Fx/HjfE9M9kznApCLcU0DvnPchazMQ==} engines: {node: ^14 || ^16 || >=18} @@ -7944,6 +7967,10 @@ snapshots: '@pkgjs/parseargs@0.11.0': optional: true + '@playwright/test@1.47.0': + dependencies: + playwright: 1.47.0 + '@pmmmwh/react-refresh-webpack-plugin@0.5.15(@types/webpack@4.41.39)(react-refresh@0.14.2)(type-fest@0.21.3)(webpack-dev-server@5.1.0(webpack@5.92.1(@swc/core@1.7.23)(esbuild@0.23.1)))(webpack@5.92.1(@swc/core@1.7.23)(esbuild@0.23.1))': dependencies: ansi-html: 0.0.9 @@ -9836,6 +9863,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -11101,6 +11131,14 @@ snapshots: dependencies: find-up: 6.3.0 + playwright-core@1.47.0: {} + + playwright@1.47.0: + dependencies: + playwright-core: 1.47.0 + optionalDependencies: + fsevents: 2.3.2 + postcss-attribute-case-insensitive@6.0.3(postcss@8.4.45): dependencies: postcss: 8.4.45