[web] add browser tests for editing and search #194
No files matched your search
+36
-20
@@ -5,33 +5,36 @@ on:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- '@luzzle/web@*'
|
||||
- "@luzzle/web@*"
|
||||
paths:
|
||||
- 'apps/web/**'
|
||||
- 'packages/web/config/**'
|
||||
- 'packages/web/theme/**'
|
||||
- 'packages/web/db/**'
|
||||
- 'packages/web/jobs/**'
|
||||
- 'packages/web/pieces/**'
|
||||
- 'packages/web/templates/**'
|
||||
- 'apps/core/**'
|
||||
- "apps/web/**"
|
||||
- "packages/web/config/**"
|
||||
- "packages/web/theme/**"
|
||||
- "packages/web/db/**"
|
||||
- "packages/web/jobs/**"
|
||||
- "packages/web/pieces/**"
|
||||
- "packages/web/templates/**"
|
||||
- "apps/core/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- 'apps/web/**'
|
||||
- 'packages/web/config/**'
|
||||
- 'packages/web/theme/**'
|
||||
- 'packages/web/db/**'
|
||||
- 'packages/web/jobs/**'
|
||||
- 'packages/web/pieces/**'
|
||||
- 'packages/web/templates/**'
|
||||
- 'apps/core/**'
|
||||
- '.github/workflows/web.yml'
|
||||
- "apps/web/**"
|
||||
- "packages/web/config/**"
|
||||
- "packages/web/theme/**"
|
||||
- "packages/web/db/**"
|
||||
- "packages/web/jobs/**"
|
||||
- "packages/web/pieces/**"
|
||||
- "packages/web/templates/**"
|
||||
- "apps/core/**"
|
||||
- ".github/workflows/web.yml"
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
# Keep the image's browser revision aligned with @playwright/test.
|
||||
container:
|
||||
image: ghcr.io/eleith/containers-playwright:1.61.1-node26@sha256:31ca7ae8b89997e60b60dec15487ea25ac9ea7b1eea45ea967f0f873c89af1ab
|
||||
defaults:
|
||||
run:
|
||||
working-directory: apps/web
|
||||
@@ -45,8 +48,8 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '26'
|
||||
cache: 'pnpm'
|
||||
node-version: "26"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Install dependencies
|
||||
working-directory: .
|
||||
@@ -75,6 +78,19 @@ jobs:
|
||||
- name: Built-in template HTTP/SSR integration
|
||||
run: pnpm run test:templates
|
||||
|
||||
- name: Browser tests
|
||||
# The HTTP integration suite already built the real app from this checkout.
|
||||
run: pnpm run test:e2e
|
||||
|
||||
- name: Browser failure artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: web-browser-failures
|
||||
path: apps/web/test-results
|
||||
if-no-files-found: ignore
|
||||
retention-days: 7
|
||||
|
||||
build-push:
|
||||
needs: check
|
||||
if: github.event_name != 'pull_request'
|
||||
|
||||
@@ -60,7 +60,8 @@ steps:
|
||||
- pnpm run check
|
||||
|
||||
test:
|
||||
image: node:26
|
||||
# Same browser version as @playwright/test, using the Gitea mirror.
|
||||
image: git.eleith.com/eleith.private/playwright:1.61.1-node26
|
||||
environment:
|
||||
COREPACK_HOME: /workspace/src/.corepack
|
||||
commands:
|
||||
@@ -68,6 +69,7 @@ steps:
|
||||
- corepack enable
|
||||
- pnpm --filter @luzzle/web test
|
||||
- pnpm --filter @luzzle/web test:templates
|
||||
- pnpm --filter @luzzle/web test:e2e
|
||||
|
||||
check-version:
|
||||
image: node:26
|
||||
|
||||
@@ -7,6 +7,7 @@ node_modules
|
||||
.vercel
|
||||
.svelte-kit/
|
||||
/build
|
||||
/test-results/
|
||||
/package-lock.json
|
||||
/src/lib/ui/styles/theme.build.css
|
||||
/src/lib/pieces/components/custom
|
||||
|
||||
@@ -6,5 +6,6 @@ yarn.lock
|
||||
**/*.json
|
||||
sample
|
||||
docker-compose.yml
|
||||
test-results
|
||||
data
|
||||
archive
|
||||
@@ -0,0 +1,22 @@
|
||||
import { expect, test } from './support/test.js'
|
||||
import { ADMIN, notebook } from './support/fixture.js'
|
||||
|
||||
test('sign in, save an edit and reload the saved source', async ({ page, baseURL }) => {
|
||||
await page.goto(notebook.editorUrl)
|
||||
await page.locator('#svelte-announcer').waitFor({ state: 'attached' })
|
||||
await page.getByLabel('Username', { exact: true }).fill(ADMIN.username)
|
||||
await page.getByLabel('Password', { exact: true }).fill(ADMIN.password)
|
||||
await page.getByRole('button', { name: 'Sign in', exact: true }).click()
|
||||
await expect(page).toHaveURL(`${baseURL}${notebook.editorUrl}`)
|
||||
|
||||
const editor = page.getByRole('textbox')
|
||||
const save = page.getByRole('button', { name: 'Save', exact: true })
|
||||
const observation = 'A saved observation from the browser.'
|
||||
await expect(editor).toContainText('Original observation.')
|
||||
await editor.press('ControlOrMeta+End')
|
||||
await page.keyboard.insertText(`\n\n${observation}`)
|
||||
await save.click()
|
||||
await expect(save).toBeDisabled()
|
||||
await page.reload()
|
||||
await expect(editor).toContainText(observation)
|
||||
})
|
||||
@@ -0,0 +1,23 @@
|
||||
import { expect, test } from './support/test.js'
|
||||
import { notebook } from './support/fixture.js'
|
||||
|
||||
test('search for a piece and open its public page', async ({ page, baseURL }) => {
|
||||
await page.goto('/')
|
||||
const search = page
|
||||
.getByRole('navigation', { name: 'Global' })
|
||||
.getByLabel('search', { exact: true })
|
||||
await expect(search).toHaveAttribute('aria-haspopup', 'dialog')
|
||||
await search.click()
|
||||
const dialog = page.getByRole('dialog', { name: 'Search', exact: true })
|
||||
await dialog.getByRole('searchbox', { name: 'Search query' }).fill(notebook.title)
|
||||
await dialog.getByRole('button', { name: 'Search', exact: true }).click()
|
||||
await expect(page).toHaveURL(
|
||||
`${baseURL}/search?${new URLSearchParams({ query: notebook.title })}`
|
||||
)
|
||||
|
||||
const results = page.getByRole('region', { name: 'Search results', exact: true })
|
||||
await expect(results.getByRole('link', { name: 'Forest Notebook' })).toHaveCount(0)
|
||||
await results.getByRole('link', { name: notebook.title }).click()
|
||||
await expect(page).toHaveURL(`${baseURL}${notebook.publicUrl}`)
|
||||
await expect(page.getByRole('heading', { name: notebook.title, exact: true })).toBeVisible()
|
||||
})
|
||||
@@ -0,0 +1,28 @@
|
||||
import { createServer } from 'node:http'
|
||||
import { once } from 'node:events'
|
||||
import type { AddressInfo } from 'node:net'
|
||||
import { createFixture } from './support/fixture.ts'
|
||||
|
||||
const server = createServer()
|
||||
function shutdown() {
|
||||
server.close(() => process.exit(0))
|
||||
server.closeIdleConnections()
|
||||
setTimeout(() => process.exit(1), 5000).unref()
|
||||
}
|
||||
process.on('SIGTERM', shutdown)
|
||||
process.on('SIGINT', shutdown)
|
||||
process.on('disconnect', shutdown)
|
||||
|
||||
// Bind before writing config so the browser, auth and Kit share the actual origin.
|
||||
server.listen(0, '127.0.0.1')
|
||||
await once(server, 'listening')
|
||||
const origin = `http://localhost:${(server.address() as AddressInfo).port}`
|
||||
await createFixture(process.cwd(), origin)
|
||||
process.env.ORIGIN = origin
|
||||
process.env.PROTOCOL_HEADER = 'x-forwarded-proto'
|
||||
const { handler } = await import(new URL('../build/handler.js', import.meta.url).href)
|
||||
server.on('request', (request, response) => {
|
||||
request.headers['x-forwarded-proto'] = 'http'
|
||||
handler(request, response)
|
||||
})
|
||||
process.send!({ origin })
|
||||
@@ -0,0 +1,95 @@
|
||||
import { mkdir, writeFile } from 'node:fs/promises'
|
||||
import path from 'node:path'
|
||||
import { asCoreDatabase, migrate, Pieces, StorageFileSystem } from '@luzzle/core'
|
||||
import { createAppDb, runWebMigrations } from '@luzzle/web.db'
|
||||
|
||||
export const ADMIN = { username: 'admin', password: 'e2e-only-admin-password' }
|
||||
export const notebook = {
|
||||
file: 'reading/meteor notes.notes.md',
|
||||
editorUrl: '/admin/piece/reading/meteor%20notes.notes.md/source',
|
||||
publicUrl: '/pieces/notes/meteor-notebook',
|
||||
type: 'notes',
|
||||
slug: 'meteor-notebook',
|
||||
title: 'Meteor Notebook'
|
||||
}
|
||||
|
||||
const pieces = [
|
||||
notebook,
|
||||
{
|
||||
file: 'forest.notes.md',
|
||||
type: 'notes',
|
||||
slug: 'forest-notebook',
|
||||
title: 'Forest Notebook'
|
||||
}
|
||||
]
|
||||
|
||||
export async function createFixture(directory: string, origin: string) {
|
||||
const dateConsumed = '2024-01-01'
|
||||
const consumedAt = Date.parse(`${dateConsumed}T00:00:00Z`)
|
||||
const archive = path.join(directory, 'archive')
|
||||
await mkdir(path.join(archive, '.luzzle/schemas'), { recursive: true })
|
||||
await mkdir(path.join(archive, 'reading'), { recursive: true })
|
||||
await mkdir(path.join(directory, 'data'), { recursive: true })
|
||||
await writeFile(
|
||||
path.join(archive, '.luzzle/schemas/notes.json'),
|
||||
JSON.stringify({
|
||||
title: 'notes',
|
||||
type: 'object',
|
||||
properties: {
|
||||
title: { type: 'string' },
|
||||
date: { type: 'string', format: 'date' }
|
||||
},
|
||||
required: ['title', 'date'],
|
||||
additionalProperties: false
|
||||
})
|
||||
)
|
||||
for (const piece of pieces) {
|
||||
await writeFile(
|
||||
path.join(archive, piece.file),
|
||||
`---\ntitle: ${piece.title}\ndate: ${dateConsumed}\n---\n\nOriginal observation.\n`
|
||||
)
|
||||
}
|
||||
const database = path.join(directory, 'data/luzzle.sqlite')
|
||||
await writeFile(
|
||||
path.join(directory, 'config.yaml'),
|
||||
JSON.stringify({
|
||||
url: { app: origin },
|
||||
storage: { root: archive },
|
||||
paths: { database },
|
||||
content: { text: { title: 'E2E Garden' } },
|
||||
auth: { secret: 'e2e-only-session-secret-at-least-32-characters', credentials: ADMIN },
|
||||
pieces: [{ type: 'notes', fields: { title: 'title', date_consumed: 'date' } }]
|
||||
})
|
||||
)
|
||||
const db = createAppDb(database)
|
||||
try {
|
||||
const core = await migrate(asCoreDatabase(db))
|
||||
if (core.error) throw core.error
|
||||
const web = await runWebMigrations(db)
|
||||
if (web.error) throw web.error
|
||||
const schemas = new Pieces(new StorageFileSystem(archive))
|
||||
for await (const result of await schemas.sync(asCoreDatabase(db))) {
|
||||
if (result.error) throw new Error(result.message)
|
||||
}
|
||||
// Seed the published read model directly; no Publish job is being simulated.
|
||||
await db
|
||||
.insertInto('web_pieces')
|
||||
.values(
|
||||
pieces.map((piece) => ({
|
||||
id: piece.slug,
|
||||
key: piece.slug,
|
||||
slug: piece.slug,
|
||||
type: piece.type,
|
||||
file_path: piece.file,
|
||||
title: piece.title,
|
||||
note: 'Original observation.',
|
||||
json_metadata: JSON.stringify({ title: piece.title, date: dateConsumed }),
|
||||
date_added: consumedAt,
|
||||
date_consumed: consumedAt
|
||||
}))
|
||||
)
|
||||
.execute()
|
||||
} finally {
|
||||
await db.destroy()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { fork } from 'node:child_process'
|
||||
import { once } from 'node:events'
|
||||
import { access, mkdir, mkdtemp, rm } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { test as base } from '@playwright/test'
|
||||
|
||||
export { expect } from '@playwright/test'
|
||||
|
||||
type AppServer = { origin: string; output: () => string }
|
||||
|
||||
export const test = base.extend<{ serverLogs: void }, { appServer: AppServer }>({
|
||||
appServer: [
|
||||
// Playwright requires destructuring even without fixture dependencies.
|
||||
// eslint-disable-next-line no-empty-pattern
|
||||
async ({}, use) => {
|
||||
await access(new URL('../../build/handler.js', import.meta.url))
|
||||
const cache = fileURLToPath(new URL('../../node_modules/.cache/', import.meta.url))
|
||||
await mkdir(cache, { recursive: true })
|
||||
const directory = await mkdtemp(`${cache}e2e-`)
|
||||
const child = fork(new URL('../server.ts', import.meta.url), [], {
|
||||
cwd: directory,
|
||||
env: { PATH: process.env.PATH, NODE_ENV: 'production', TZ: 'UTC' },
|
||||
execArgv: [],
|
||||
stdio: ['ignore', 'pipe', 'pipe', 'ipc']
|
||||
})
|
||||
let output = ''
|
||||
child.stdout!.on('data', (chunk) => (output = (output + chunk).slice(-50000)))
|
||||
child.stderr!.on('data', (chunk) => (output = (output + chunk).slice(-50000)))
|
||||
try {
|
||||
const origin = await new Promise<string>((resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('E2E server startup timed out')), 30000)
|
||||
child.once('message', (message: { origin: string }) => {
|
||||
clearTimeout(timeout)
|
||||
resolve(message.origin)
|
||||
})
|
||||
child.once('error', (error) => {
|
||||
clearTimeout(timeout)
|
||||
reject(error)
|
||||
})
|
||||
child.once('exit', (code, signal) => {
|
||||
clearTimeout(timeout)
|
||||
reject(new Error(`E2E server exited (${code ?? signal})`))
|
||||
})
|
||||
})
|
||||
await use({ origin, output: () => output })
|
||||
} catch (error) {
|
||||
console.error(output)
|
||||
throw error
|
||||
} finally {
|
||||
if (child.exitCode === null && child.signalCode === null) {
|
||||
const exited = once(child, 'exit')
|
||||
child.kill('SIGTERM')
|
||||
const kill = setTimeout(() => child.kill('SIGKILL'), 6000)
|
||||
try {
|
||||
await exited
|
||||
} finally {
|
||||
clearTimeout(kill)
|
||||
}
|
||||
}
|
||||
await rm(directory, { recursive: true, force: true })
|
||||
}
|
||||
},
|
||||
{ scope: 'worker', timeout: 60000 }
|
||||
],
|
||||
baseURL: async ({ appServer }, use) => {
|
||||
await use(appServer.origin)
|
||||
},
|
||||
serverLogs: [
|
||||
async ({ appServer }, use, testInfo) => {
|
||||
await use()
|
||||
if (testInfo.status !== testInfo.expectedStatus) {
|
||||
await testInfo.attach('server.log', { body: appServer.output(), contentType: 'text/plain' })
|
||||
}
|
||||
},
|
||||
{ auto: true }
|
||||
]
|
||||
})
|
||||
@@ -48,6 +48,7 @@ export default [
|
||||
ignores: [
|
||||
'build/',
|
||||
'.svelte-kit/',
|
||||
'test-results/',
|
||||
'docker-compose.yml',
|
||||
'src/lib/pieces/components/custom/',
|
||||
'src/lib/content/components/custom/',
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"lint": "prettier --check . && prettier --config .prettierrc --check '../../packages/web/templates/src/**/*.svelte' && eslint .",
|
||||
"format": "prettier --write .",
|
||||
"test": "vitest run --silent",
|
||||
"test:templates": "vitest run --config vitest.templates.config.ts --silent"
|
||||
"test:templates": "vitest run --config vitest.templates.config.ts --silent",
|
||||
"test:e2e": "playwright test"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24"
|
||||
@@ -45,6 +46,7 @@
|
||||
"@luzzle/core": "workspace:*",
|
||||
"@luzzle/web.config": "workspace:*",
|
||||
"@luzzle/web.jobs": "workspace:*",
|
||||
"@playwright/test": "1.61.1",
|
||||
"@sveltejs/adapter-node": "6.0.1",
|
||||
"@sveltejs/enhanced-img": "1.0.0",
|
||||
"@sveltejs/kit": "3.1.1",
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './e2e',
|
||||
testMatch: '**/*.e2e.ts',
|
||||
workers: 1,
|
||||
retries: 0,
|
||||
forbidOnly: !!process.env.CI,
|
||||
outputDir: './test-results',
|
||||
use: {
|
||||
timezoneId: 'UTC',
|
||||
trace: 'retain-on-failure',
|
||||
screenshot: 'only-on-failure'
|
||||
},
|
||||
projects: [{ name: 'chromium', use: { ...devices['Desktop Chrome'] } }]
|
||||
})
|
||||
@@ -9,6 +9,8 @@
|
||||
"src",
|
||||
"scripts/**/*.test.ts",
|
||||
"test/templates/**/*.ts",
|
||||
"e2e/**/*.ts",
|
||||
"playwright.config.ts",
|
||||
"vite.config.ts",
|
||||
"vitest*.config.ts"
|
||||
],
|
||||
|
||||
@@ -42,3 +42,27 @@ It is intentionally different from the [published-image example](docker-compose.
|
||||
Follow the [development checks](../development.md). Keep parsing in `@luzzle/core`,
|
||||
heavy work in the worker, UI styling on theme tokens, and web/worker/templates on
|
||||
one exact Svelte version. Shared web packages stay internal with `workspace:*`.
|
||||
|
||||
## Browser tests
|
||||
|
||||
After building shared packages, install Chromium's headless shell and build the web app:
|
||||
|
||||
```sh
|
||||
pnpm --filter @luzzle/web exec playwright install --with-deps --only-shell chromium
|
||||
pnpm --filter @luzzle/web build
|
||||
pnpm --filter @luzzle/web test:e2e
|
||||
```
|
||||
|
||||
The Playwright suite covers two journeys: sign in to a protected editor,
|
||||
save an edit and reload it; and search for a piece and open its public page. It
|
||||
uses one Chromium worker against the production build, with a temporary
|
||||
archive/config and a real SQLite index, not your demo or live data. It runs no
|
||||
worker, LSP or external identity/storage services; it does not prove Preview/Publish
|
||||
execution.
|
||||
|
||||
GitHub and Woodpecker reuse the app build from `test:templates` in their existing
|
||||
test jobs. Both use the prebuilt Playwright image used by When/Directory: GHCR on
|
||||
GitHub, the Gitea mirror on Woodpecker. Keep the image/browser version aligned with
|
||||
`@playwright/test`. Failed journeys retain traces, screenshots and server logs
|
||||
under `apps/web/test-results`; GitHub uploads those artifacts. There is no second
|
||||
build, browser matrix or automatic retry.
|
||||
Generated
+38
@@ -354,6 +354,9 @@ importers:
|
||||
'@luzzle/web.jobs':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/web/jobs
|
||||
'@playwright/test':
|
||||
specifier: 1.61.1
|
||||
version: 1.61.1
|
||||
'@sveltejs/adapter-node':
|
||||
specifier: 6.0.1
|
||||
version: 6.0.1(@sveltejs/kit@3.1.1(@sveltejs/vite-plugin-svelte@7.1.2(svelte@5.57.3(@typescript-eslint/types@8.60.0))(vite@8.0.14(@types/node@24.12.4)(yaml@2.7.0)))(svelte@5.57.3(@typescript-eslint/types@8.60.0))(typescript@6.0.3)(vite@8.0.14(@types/node@24.12.4)(yaml@2.7.0)))
|
||||
@@ -1726,6 +1729,11 @@ packages:
|
||||
resolution: {integrity: sha512-fdDH1LSGfZdTH2sxdpVMw31BanV28K/Gry0cVFxaNP77neJSkd82mM8ErPNYs9e+0O7SdHBLTDzDgwUuy18RnQ==}
|
||||
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
|
||||
|
||||
'@playwright/test@1.61.1':
|
||||
resolution: {integrity: sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
'@pnpm/deps.graph-sequencer@1100.0.1':
|
||||
resolution: {integrity: sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A==}
|
||||
engines: {node: '>=22.13'}
|
||||
@@ -2835,6 +2843,11 @@ packages:
|
||||
resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
|
||||
engines: {node: '>=12.20.0'}
|
||||
|
||||
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}
|
||||
@@ -3561,6 +3574,16 @@ packages:
|
||||
pkg-types@2.3.1:
|
||||
resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==}
|
||||
|
||||
playwright-core@1.61.1:
|
||||
resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.61.1:
|
||||
resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -5294,6 +5317,10 @@ snapshots:
|
||||
|
||||
'@pkgr/core@0.1.2': {}
|
||||
|
||||
'@playwright/test@1.61.1':
|
||||
dependencies:
|
||||
playwright: 1.61.1
|
||||
|
||||
'@pnpm/deps.graph-sequencer@1100.0.1': {}
|
||||
|
||||
'@polka/url@1.0.0-next.29': {}
|
||||
@@ -6320,6 +6347,9 @@ snapshots:
|
||||
dependencies:
|
||||
fetch-blob: 3.2.0
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
@@ -7310,6 +7340,14 @@ snapshots:
|
||||
exsolve: 1.0.8
|
||||
pathe: 2.0.3
|
||||
|
||||
playwright-core@1.61.1: {}
|
||||
|
||||
playwright@1.61.1:
|
||||
dependencies:
|
||||
playwright-core: 1.61.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
postcss-load-config@3.1.4(postcss@8.5.15):
|
||||
|
||||
Reference in new issue
Block a user