feat(pkg): Add hda-cms-extension-e2e

This commit is contained in:
T. R. Bernstein
2025-02-26 23:57:37 +01:00
parent 07ff1edd24
commit e6a5afc3cd
23 changed files with 697 additions and 0 deletions

View File

@@ -38,3 +38,4 @@ authors = [
[packages]
hda-cms-extension = { path = "packages/hda-cms-extension" }
hda-cms-extension-e2e = { path = "packages/hda-cms-extension-e2e" }

View File

@@ -0,0 +1,2 @@
directus-config.js
docker-compose.yml

View File

@@ -0,0 +1,22 @@
ARG NODE_VERSION=22
####################################################################################################
## Build Packages
FROM node:${NODE_VERSION}-alpine AS builder
ARG TARGETPLATFORM
RUN <<EOF
if [ "$TARGETPLATFORM" = 'linux/arm64' ]; then
apk --no-cache add python3 py3-pip build-base
ln -sf /usr/bin/python3 /usr/bin/python
fi
EOF
WORKDIR /extensions
RUN --mount=type=bind,source=directus-extensions.json,target=/extensions/package.json npm install
RUN mkdir -p ./directus
RUN grep -lR "directus:extension" ./node_modules/* | grep -E 'package.json$' | sed 's/\/package.json//;' | xargs -r -I % mv % ./directus
FROM tabshift4docker/directus:latest
# Copy third party extensions
COPY --from=builder /extensions/directus ./extensions

View File

@@ -0,0 +1,24 @@
const config: {
PUBLIC_URL: string
HOST: string
PORT: number
EXTENSIONS_AUTO_RELOAD: boolean
DB_CLIENT: string
DB_FILENAME: string
SECRET: string
NODE_ENV: string
WEBSOCKETS_ENABLED: boolean
CORS_ENABLED: boolean
CORS_ORIGIN: boolean
ADMIN_EMAIL: string
ADMIN_PASSWORD: string
ADMIN_TOKEN: string
EMAIL_TRANSPORT: string
EMAIL_SMTP_HOST: string
EMAIL_SMTP_PORT: number
EMAIL_SMTP_USER: string
EMAIL_SMTP_PASSWORD: string
EMAIL_SMTP_SECURE: boolean
EMAIL_FROM: string
}
export default config

View File

@@ -0,0 +1,28 @@
export default {
PUBLIC_URL: 'http://localhost',
HOST: '0.0.0.0',
PORT: 8055,
EXTENSIONS_AUTO_RELOAD: true,
DB_CLIENT: 'sqlite3',
DB_FILENAME: '/directus/database/database.sqlite',
SECRET: '016316D6-A8C1-498B-B42D-660B8D1C7BD8',
NODE_ENV: 'development',
WEBSOCKETS_ENABLED: true,
CORS_ENABLED: true,
CORS_ORIGIN: true,
ADMIN_EMAIL: 'admin@tabshift.dev',
ADMIN_PASSWORD: 'K@QtaBG8ydC-GrTiHM',
ADMIN_TOKEN: '4437384844958328Dj83jDjdie83h212',
EMAIL_TRANSPORT: 'smtp',
EMAIL_SMTP_HOST: 'h125.hostmesh.de',
EMAIL_SMTP_PORT: 587,
EMAIL_SMTP_USER: 'notifier',
EMAIL_SMTP_PASSWORD: 'Qhh.9dxze_cMkd6NdE-erDdsT',
EMAIL_SMTP_SECURE: true,
EMAIL_FROM: '"Tabshift Notifications" <notifications@tabshift.dev>'
}

View File

@@ -0,0 +1,13 @@
{
"name": "directus-extensions",
"dependencies": {
"@directus-labs/card-select-interfaces": "^1.0.0",
"@directus-labs/command-palette-module": "^1.0.1",
"@directus-labs/experimental-m2a-interface": "^1.1.0",
"@directus-labs/simple-list-interface": "^1.0.0",
"@directus-labs/super-header-interface": "^1.1.0",
"directus-extension-group-tabs-interface": "^2.1.0",
"directus-extension-sync": "^3.0.2",
"directus-extension-wpslug-interface": "^1.1.0"
}
}

View File

@@ -0,0 +1,12 @@
name: directus
services:
directus:
image: directus-extension-hda
build: .
ports:
- 8055:8055
volumes:
- ../../hda-cms-extension:/directus/extensions/directus-extension-hda
- ./directus-config.js:/directus/extensions/directus-extension-hda/directus-config.js:r
environment:
- CONFIG_PATH=/directus/extensions/directus-extension-hda/directus-config.js

View File

@@ -0,0 +1,17 @@
{
"name": "@tabshift/hda-cms-extension-e2e",
"description": "E2E tests for the CMS exension of Haus der Akademien",
"version": "1.0.0",
"type": "module",
"scripts": {
"e2e-test": "playwright test"
},
"devDependencies": {
"@playwright/test": "latest",
"@tabshift/typescript-config": "latest",
"@types/node": "latest",
"typescript": "latest"
},
"author": "T. R. Bernstein <bhdacms01-project@tabshift.dev>",
"license": "EUPL-1.2"
}

View File

@@ -0,0 +1,85 @@
import type { PlaywrightTestConfig } from '@playwright/test'
import type { WorkerConfigOptions } from './src/fixtures/config-options.js'
import { defineConfig, devices } from '@playwright/test'
import { fileURLToPath } from 'url'
import * as path from 'path'
import directus_config from './docker/directus-config.js'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const testDir = './src'
const playwrightDir = path.join(__dirname, '.playwright')
const outputDir = path.join(playwrightDir, 'Temporaries')
const globalSessionFile = path.join(playwrightDir, 'session.js')
/**
* See https://playwright.dev/docs/test-configuration.
*/
const config: PlaywrightTestConfig<{}, WorkerConfigOptions> = {
testDir,
outputDir,
forbidOnly: !!process.env['CI'],
retries: process.env['CI'] ? 2 : 0,
workers: process.env['CI'] ? 1 : '100%',
reporter: 'list',
timeout: 60000,
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
baseURL: `${directus_config.PUBLIC_URL}:${directus_config.PORT}`,
trace: 'on-first-retry',
globalSessionFile
},
projects: [
{
name: 'setup',
testMatch: /.*\.setup\.ts/
},
{
name: 'chromium',
use: {
...devices['Desktop Chrome']
},
dependencies: ['setup']
},
{
name: 'firefox',
use: {
...devices['Desktop Firefox']
},
dependencies: ['setup']
},
{
name: 'webkit',
use: {
...devices['Desktop Safari']
},
dependencies: ['setup']
},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: {
...devices['Pixel 5']
},
dependencies: ['setup']
},
{
name: 'Mobile Safari',
use: {
...devices['iPhone 12']
},
dependencies: ['setup']
}
],
/* Run your local dev server before starting the tests */
webServer: {
command: './setup.sh start-fg',
url: `${directus_config.PUBLIC_URL}:${directus_config.PORT}`,
reuseExistingServer: !process.env['CI'],
gracefulShutdown: { signal: 'SIGTERM', timeout: 2000 }
}
}
export default defineConfig(config)

View File

@@ -0,0 +1,74 @@
#!/usr/bin/env zsh
TABSHIFT_DIRECTUS_PATH="${0:h}/../../apps/directus"
TABSHIFT_DIRECTUS_NAME='tabshift4docker/directus'
DOCKER_E2E_SETUP_PATH="${0:h}/docker"
start_docker_runtime() {
colima status || colima start
}
stop_docker_runtime() {
colima status && colima stop
}
build_tabshift_directus_image() {
if docker image ls | grep ${TABSHIFT_DIRECTUS_NAME}; then
return
fi
docker buildx build -t ${TABSHIFT_DIRECTUS_NAME}:latest --platform linux/arm64 $TABSHIFT_DIRECTUS_PATH
}
start_docker_compose_service() {
pushd -q ${DOCKER_E2E_SETUP_PATH}
docker-compose up ${start_in_background:+'-d'}
popd -q
}
stop_docker_compose_services() {
pushd -q ${DOCKER_E2E_SETUP_PATH}
docker-compose stop
popd -q
}
start_docker_compose_service_and_runtime() {
local start_in_background=${1:+'y'}
start_docker_runtime
build_tabshift_directus_image
start_docker_compose_service
}
start_docker_compose_service_in_background() {
start_docker_compose_service_and_runtime start_in_background
}
start_docker_compose_service_in_foreground() {
start_docker_compose_service_and_runtime
}
stop_docker_compose_services_and_runtime() {
stop_docker_compose_services
stop_docker_runtime
}
trap 'docker container prune -f' INT TERM EXIT
main() {
local CMD=$1
case $CMD in
start)
start_docker_compose_service_in_background
;;
start-fg)
start_docker_compose_service_in_foreground
;;
stop-all)
stop_docker_compose_services_and_runtime
;;
*)
stop_docker_compose_services
;;
esac
}
main $*

View File

@@ -0,0 +1,8 @@
import { expect, test } from '../fixtures/worker-specific-admin-user.js'
import { navigateTo } from '../fixtures/navigation.js'
test('Antrago FTP Import operation extension is listed on extension page', async ({ workerAdminPage }) => {
const page = workerAdminPage
await navigateTo(page, '/settings/extensions')
await expect(page.getByRole('main')).toContainText('antrago-ftp-import')
})

View File

@@ -0,0 +1,61 @@
import type { Page } from '@playwright/test'
import { expect, test } from '../fixtures/page-with-flow.js'
import { navigateTo } from '../fixtures/navigation.js'
import { getFlowNameForWorker } from '../fixtures/page-with-flow.js'
import enLang from '../../../hda-cms-extension/src/antrago-ftp-import/i18n/en-US.json' with { type: 'json' }
import deLang from '../../../hda-cms-extension/src/antrago-ftp-import/i18n/de-DE.json' with { type: 'json' }
import { setLanguageToGerman, setLanguageToEnglish } from '../fixtures/language-setter.js'
import { getAccountForWorker } from '../fixtures/worker-specific-admin-user.js'
async function openOperationListOfFlow(page: Page, flowName: string) {
await navigateTo(page, '/settings/flows')
await page.getByText(flowName).click()
await page.getByRole('main').locator('i').nth(4).click()
}
async function verifyOperationIsListed(page: Page, operationName: string) {
await page.reload()
await expect(page.getByRole('article')).toContainText(operationName)
}
test('FTP import operation is listed in flow items list in english & german', async ({
globalSessionPage,
pageWithFlow
}, testInfo) => {
const page = pageWithFlow
const account = getAccountForWorker(testInfo.workerIndex)
const flowName = getFlowNameForWorker(testInfo.workerIndex)
await openOperationListOfFlow(page, flowName)
await verifyOperationIsListed(page, enLang.name)
await setLanguageToGerman(globalSessionPage, account.fullname)
await verifyOperationIsListed(page, deLang.name)
await setLanguageToEnglish(globalSessionPage, account.fullname)
})
test('FTP import operation has "FTP Server" config field', async ({ pageWithFlow }, testInfo) => {
const page = pageWithFlow
const flowName = getFlowNameForWorker(testInfo.workerIndex)
await openOperationListOfFlow(page, flowName)
await page.getByText(enLang.name).click()
await expect(page.getByRole('article')).toContainText(enLang.options.ftpserver.name)
await expect(page.getByRole('article')).toContainText(enLang.options.ftpuser.name)
await expect(page.getByRole('article')).toContainText(enLang.options.ftppassword.name)
})
test('FTP import operation shows config details in box', async ({ pageWithFlow }, testInfo) => {
const page = pageWithFlow
const flowName = getFlowNameForWorker(testInfo.workerIndex)
await openOperationListOfFlow(page, flowName)
await page.getByText(enLang.name).click()
await page.locator('.interface > .v-input > .input').first().click()
await page.getByRole('textbox').nth(2).fill('Some Server')
await page.getByRole('textbox').nth(2).press('Tab')
await page.getByRole('textbox').nth(3).fill('Some Username')
await page.getByRole('textbox').nth(3).press('Tab')
await page.locator('input[type="password"]').fill('Some Password')
await page.locator('#dialog-outlet').getByRole('button', { name: 'check' }).click()
await expect(page.getByRole('main').first()).toContainText('Some Username@Some Server')
})

View File

@@ -0,0 +1,9 @@
import { expect, test } from './fixtures/worker-specific-admin-user.js'
import packageInfo from '../../hda-cms-extension/package.json' with { type: 'json' }
import { navigateTo } from './fixtures/navigation.js'
test('bundle extension is listed on extension page', async ({ workerAdminPage }) => {
const page = workerAdminPage
await navigateTo(page, '/settings/extensions')
await expect(page.getByRole('main')).toContainText(packageInfo.name)
})

View File

@@ -0,0 +1,10 @@
import { test as base } from '@playwright/test'
export interface WorkerConfigOptions {
globalSessionFile: string
}
export * from '@playwright/test'
export const test = base.extend<{}, WorkerConfigOptions>({
globalSessionFile: ['.playwright/session.js', { scope: 'worker', option: true }]
})

View File

@@ -0,0 +1,31 @@
import type { Page } from '@playwright/test'
import { navigateTo } from './navigation.js'
const searchOptions = {
'de-DE': {
searchStr: 'ger',
select: 'German (Germany)'
},
'en-US': {
searchStr: 'engl',
select: 'English (United States)'
}
}
async function setLanguage(page: Page, user: string, language: keyof typeof searchOptions) {
const options = searchOptions[language]
await navigateTo(page, '/users')
await page.getByText(user).click()
await page.locator('.v-menu-activator > .v-input > .input').first().click()
await page.getByRole('textbox', { name: 'Search' }).fill(options.searchStr)
await page.getByText(options.select).click()
await page.getByRole('button', { name: 'check', exact: true }).click()
}
export async function setLanguageToGerman(page: Page, user: string) {
await setLanguage(page, user, 'de-DE')
}
export async function setLanguageToEnglish(page: Page, user: string) {
await setLanguage(page, user, 'en-US')
}

View File

@@ -0,0 +1,59 @@
import type { Page } from '@playwright/test'
type ButtonName = string | [string, boolean]
interface NavigationOption {
navigationButtonNames: ButtonName[]
}
const navigationDestinations = {
'/settings/flows': {
navigationButtonNames: [['settings', true], 'bolt Flows']
} as NavigationOption,
'/users': {
navigationButtonNames: ['people_alt']
} as NavigationOption,
'/settings/project': {
navigationButtonNames: [['settings', true], 'tune Settings']
} as NavigationOption,
'/settings/extensions': {
navigationButtonNames: [['settings', true], 'category Extensions']
} as NavigationOption
}
let isMobile: boolean | undefined = undefined
async function openMobileMenu(page: Page) {
const hamburgerButton = page.getByRole('button', { name: 'menu' })
if (isMobile != false) {
try {
await hamburgerButton.click({ timeout: 1000 })
if (isMobile == undefined) isMobile = true
} catch {
if (isMobile == undefined) isMobile = false
}
}
}
async function navigateUsingMenu(page: Page, buttonNames: ButtonName[]) {
for (let buttonName of buttonNames) {
let isExact = false
if (typeof buttonName != 'string') {
isExact = buttonName[1]
buttonName = buttonName[0]
}
openMobileMenu(page)
await page.getByRole('link', { name: buttonName, exact: isExact }).first().click({ timeout: 1000 })
}
}
export async function navigateTo(page: Page, link: keyof typeof navigationDestinations) {
const navigationOption = navigationDestinations[link]
try {
await navigateUsingMenu(page, navigationOption.navigationButtonNames)
} catch {
await page.goto(`/admin${link}`)
}
await page.reload()
}

View File

@@ -0,0 +1,67 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import directusConfig from '../../docker/directus-config.js'
export type Account = {
username: string
fullname: string
password: string
}
export async function loginAsAccount(page: Page, account: Account) {
await page.goto('/admin/login')
try {
await page.getByRole('button', { name: 'Continue' }).click({ timeout: 300 })
} catch {
await page.getByRole('textbox', { name: 'Email' }).click()
await page.getByRole('textbox', { name: 'Email' }).fill(account.username)
await page.getByRole('textbox', { name: 'Password' }).click()
await page.getByRole('textbox', { name: 'Password' }).fill(account.password)
await page.getByRole('button', { name: 'Sign In' }).click()
}
await page.waitForURL(/.*\/admin\/(?!login|logout)/g)
}
export async function createAdminAccount(page: Page, account: Account) {
await page.goto('/admin/users')
try {
await expect(page.getByRole('main')).not.toContainText(account.username)
} catch {
return
}
await page.getByRole('link', { name: 'add' }).click()
await page.locator('input').first().click()
await page.locator('input').first().fill(account.fullname)
await page.locator('input').nth(2).click()
await page.locator('input').nth(2).fill(account.username)
await page.locator('input[type="password"]').first().click()
await page.locator('input[type="password"]').first().fill(account.password)
await page
.locator('div')
.filter({ hasText: /^Role$/ })
.first()
.click()
await expect(page.getByText('Directus RolesSelect')).toBeVisible()
await page.getByRole('checkbox', { name: 'radio_button_unchecked' }).click()
await page.locator('#dialog-outlet').getByRole('button', { name: 'check' }).click()
await page.getByRole('button', { name: 'check', exact: true }).first().click()
}
export async function logout(page: Page) {
await page.goto('/admin/users')
await page.getByRole('link', { name: 'account_circle' }).hover()
await page.getByRole('navigation', { name: 'Module Navigation' }).getByRole('button').nth(1).hover()
await page.getByRole('navigation', { name: 'Module Navigation' }).getByRole('button').nth(1).click()
await page.getByRole('link', { name: 'Sign Out' }).click()
await page.waitForURL(/.*\/admin\/login/g)
}
export async function loginAsMainAdmin(page: Page) {
await loginAsAccount(page, {
username: directusConfig.ADMIN_EMAIL,
password: directusConfig.ADMIN_PASSWORD
})
}

View File

@@ -0,0 +1,34 @@
import type { Page } from '@playwright/test'
import { test as base } from './worker-specific-admin-user.js'
import { navigateTo } from './navigation.js'
async function createFlow(page: Page, name: string) {
await navigateTo(page, '/settings/flows')
await page.getByRole('button', { name: 'add' }).first().click()
await page.getByRole('textbox', { name: 'Name' }).fill(name)
await page.getByRole('button', { name: 'arrow_forward' }).click()
await page
.locator('div')
.filter({ hasText: /^ManualTriggers on manual run within selected collection\(s\)$/ })
.first()
.click()
await page.getByRole('button', { name: 'check' }).click()
await page.getByRole('button', { name: 'check', exact: true }).first().click()
}
export function getFlowNameForWorker(workerIndex: number) {
return `Flow #${workerIndex}`
}
export * from '@playwright/test'
export const test = base.extend<{}, { flowName: string; pageWithFlow: Page }>({
pageWithFlow: [
async ({ workerAdminPage }, use, workerInfo) => {
const page = workerAdminPage
const flowName = getFlowNameForWorker(workerInfo.workerIndex)
await createFlow(page, flowName)
await use(page)
},
{ scope: 'worker', title: 'Worker Flow Page' }
]
})

View File

@@ -0,0 +1,56 @@
import type { Browser, Page, WorkerInfo } from '@playwright/test'
import * as fs from 'fs'
import * as path from 'path'
import { test as base } from './config-options.js'
function getSessionFilenameForWorker(worker: number): string {
const filename = path.resolve(test.info().project.outputDir, `session-data_worker-${worker}.js`)
return filename
}
async function getSessionPage(browser: Browser, sessionFile: string) {
if (fs.existsSync(sessionFile)) {
return await browser.newPage({ storageState: sessionFile })
} else {
return await browser.newPage()
}
}
async function storeSession(page: Page, sessionFile: string) {
const context = page.context()
await context.storageState({ path: sessionFile })
}
async function getPageFixture(browser: Browser, use: (r: Page) => Promise<void>, sessionFile: string) {
const page = await getSessionPage(browser, sessionFile)
await use(page)
await storeSession(page, sessionFile)
await page.close()
}
async function getWorkerSessionPage(
{ browser }: { browser: Browser },
use: (r: Page) => Promise<void>,
workerInfo: WorkerInfo
) {
const workerIndex = workerInfo.workerIndex
const sessionFile = getSessionFilenameForWorker(workerIndex)
await getPageFixture(browser, use, sessionFile)
}
async function getGlobalSessionPage(
{ browser, globalSessionFile }: { browser: Browser; globalSessionFile: string },
use: (r: Page) => Promise<void>
) {
await getPageFixture(browser, use, globalSessionFile)
}
export interface SessionPageOptions {
workerSessionPage: Page
globalSessionPage: Page
}
export * from '@playwright/test'
export const test = base.extend<{}, SessionPageOptions>({
workerSessionPage: [getWorkerSessionPage, { scope: 'worker', title: 'Worker Session Page' }],
globalSessionPage: [getGlobalSessionPage, { scope: 'worker', title: 'Global Session Page' }]
})

View File

@@ -0,0 +1,25 @@
import type { Page } from '@playwright/test'
import type { Account } from './page-actions.js'
import { test as base } from './session-page.js'
import { createAdminAccount, loginAsAccount } from './page-actions.js'
export function getAccountForWorker(worker: number): Account {
return {
username: `user${worker}@example.com`,
fullname: `Worker #${worker}`,
password: 'sameoldpasswordwithextrasteps'
}
}
export * from '@playwright/test'
export const test = base.extend<{}, { workerAdminPage: Page }>({
workerAdminPage: [
async ({ globalSessionPage, workerSessionPage }, use, workerInfo) => {
const workerAdminAccount = getAccountForWorker(workerInfo.workerIndex)
await createAdminAccount(globalSessionPage, workerAdminAccount)
await loginAsAccount(workerSessionPage, workerAdminAccount)
await use(workerSessionPage)
},
{ scope: 'worker', title: 'Worker Admin User' }
]
})

View File

@@ -0,0 +1,6 @@
import { test as setup } from './fixtures/session-page.js'
import { loginAsMainAdmin } from './fixtures/page-actions.js'
setup('Authenticate as main admin', async ({ globalSessionPage }) => {
await loginAsMainAdmin(globalSessionPage)
})

View File

@@ -0,0 +1,3 @@
{
"extends": "@tabshift/typescript-config/base.json"
}

50
pnpm-lock.yaml generated
View File

@@ -30,6 +30,21 @@ importers:
specifier: ^5.7.3
version: 5.7.3
packages/hda-cms-extension-e2e:
devDependencies:
'@playwright/test':
specifier: latest
version: 1.50.1
'@tabshift/typescript-config':
specifier: latest
version: 1.0.0
'@types/node':
specifier: latest
version: 22.13.5
typescript:
specifier: latest
version: 5.7.3
packages:
'@babel/code-frame@7.26.2':
@@ -527,6 +542,11 @@ packages:
'@jridgewell/trace-mapping@0.3.25':
resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==}
'@playwright/test@1.50.1':
resolution: {integrity: sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==}
engines: {node: '>=18'}
hasBin: true
'@rollup/plugin-commonjs@25.0.8':
resolution: {integrity: sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==}
engines: {node: '>=14.0.0'}
@@ -1054,6 +1074,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}
@@ -1403,6 +1428,16 @@ packages:
typescript:
optional: true
playwright-core@1.50.1:
resolution: {integrity: sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==}
engines: {node: '>=18'}
hasBin: true
playwright@1.50.1:
resolution: {integrity: sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==}
engines: {node: '>=18'}
hasBin: true
postcss-calc@8.2.4:
resolution: {integrity: sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q==}
peerDependencies:
@@ -2329,6 +2364,10 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.0
'@playwright/test@1.50.1':
dependencies:
playwright: 1.50.1
'@rollup/plugin-commonjs@25.0.8(rollup@3.29.5)':
dependencies:
'@rollup/pluginutils': 5.1.4(rollup@3.29.5)
@@ -2919,6 +2958,9 @@ snapshots:
fs.realpath@1.0.0: {}
fsevents@2.3.2:
optional: true
fsevents@2.3.3:
optional: true
@@ -3217,6 +3259,14 @@ snapshots:
transitivePeerDependencies:
- '@vue/composition-api'
playwright-core@1.50.1: {}
playwright@1.50.1:
dependencies:
playwright-core: 1.50.1
optionalDependencies:
fsevents: 2.3.2
postcss-calc@8.2.4(postcss@8.5.3):
dependencies:
postcss: 8.5.3