diff --git a/.gitignore b/.gitignore index 2ac0d42a14..088120a39a 100644 --- a/.gitignore +++ b/.gitignore @@ -151,6 +151,9 @@ packages/*/docs/ packaging/dist +# E2E test temp directories and artifacts +.e2e-tmp + # Shadowenv generates user-specific files that shouldn't be committed .shadowenv.d/ diff --git a/package.json b/package.json index 43063efb40..14bac929ac 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "post-release": "./bin/post-release", "shopify:run": "node packages/cli/bin/dev.js", "shopify": "nx build cli && node packages/cli/bin/dev.js", + "test:e2e": "nx run-many --target=build --projects=cli,create-app --skip-nx-cache && pnpm --filter e2e exec playwright test", "test:features": "pnpm nx run features:test", "test:regenerate-snapshots": "nx build cli && packages/features/snapshots/regenerate.sh", "test:unit": "pnpm vitest run", @@ -130,7 +131,8 @@ "@graphql-typed-document-node/core" ], "ignoreWorkspaces": [ - "packages/eslint-plugin-cli" + "packages/eslint-plugin-cli", + "packages/e2e" ], "paths": { "@shopify/eslint-plugin-cli/configs": [ diff --git a/packages/e2e/.env.example b/packages/e2e/.env.example new file mode 100644 index 0000000000..5af4efc9c9 --- /dev/null +++ b/packages/e2e/.env.example @@ -0,0 +1,19 @@ +# Required: Client ID of the primary test app (must be in the genghis account's org) +# CI secret: E2E_CLIENT_ID +SHOPIFY_FLAG_CLIENT_ID= + +# Required: Genghis account email for browser-based OAuth login +# CI secret: E2E_ACCOUNT_EMAIL +E2E_ACCOUNT_EMAIL= + +# Required: Genghis account password for browser-based OAuth login +# CI secret: E2E_ACCOUNT_PASSWORD +E2E_ACCOUNT_PASSWORD= + +# Required: Dev store FQDN for dev server / deploy tests (e.g. my-store.myshopify.com) +# CI secret: E2E_STORE_FQDN +E2E_STORE_FQDN= + +# Optional: Client ID of a secondary app for config link tests +# CI secret: E2E_SECONDARY_CLIENT_ID +E2E_SECONDARY_CLIENT_ID= diff --git a/packages/e2e/.gitignore b/packages/e2e/.gitignore new file mode 100644 index 0000000000..502ee2939d --- /dev/null +++ b/packages/e2e/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +test-results/ +playwright-report/ +dist/ +.env +.env.local diff --git a/packages/e2e/helpers/strip-ansi.ts b/packages/e2e/helpers/strip-ansi.ts new file mode 100644 index 0000000000..ae42093841 --- /dev/null +++ b/packages/e2e/helpers/strip-ansi.ts @@ -0,0 +1,5 @@ +// Re-export strip-ansi as a named export for easier use. +// strip-ansi v7+ is ESM-only and exports a default function. +import stripAnsiModule from 'strip-ansi' + +export const stripAnsi: (text: string) => string = stripAnsiModule diff --git a/packages/e2e/package.json b/packages/e2e/package.json new file mode 100644 index 0000000000..ff126b8d52 --- /dev/null +++ b/packages/e2e/package.json @@ -0,0 +1,39 @@ +{ + "name": "@shopify/e2e", + "version": "0.1.0", + "packageManager": "pnpm@10.11.1", + "private": true, + "type": "module", + "scripts": { + "test": "nx run e2e:test", + "lint": "nx lint", + "lint:fix": "nx lint:fix", + "type-check": "nx type-check" + }, + "eslintConfig": { + "extends": [ + "../../.eslintrc.cjs" + ], + "rules": { + "no-console": "off", + "import/extensions": [ + "error", + "never", + { + "ignorePackages": true + } + ] + } + }, + "devDependencies": { + "@playwright/test": "^1.50.0", + "@types/node": "18.19.70", + "execa": "^7.2.0", + "node-pty": "^1.0.0", + "strip-ansi": "^7.1.0", + "tempy": "^1.0.1" + }, + "engines": { + "node": ">=20.10.0" + } +} diff --git a/packages/e2e/playwright.config.ts b/packages/e2e/playwright.config.ts new file mode 100644 index 0000000000..f2260472c5 --- /dev/null +++ b/packages/e2e/playwright.config.ts @@ -0,0 +1,42 @@ +/* eslint-disable line-comment-position */ +/* eslint-disable no-restricted-imports */ +import {defineConfig} from '@playwright/test' +import * as fs from 'fs' +import * as path from 'path' +import {fileURLToPath} from 'url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) + +// Load .env file if present (CI provides env vars directly) +const envPath = path.join(__dirname, '.env') +if (fs.existsSync(envPath)) { + for (const line of fs.readFileSync(envPath, 'utf-8').split('\n')) { + const trimmed = line.trim() + if (!trimmed || trimmed.startsWith('#')) continue + const eqIdx = trimmed.indexOf('=') + if (eqIdx === -1) continue + const key = trimmed.slice(0, eqIdx).trim() + const value = trimmed.slice(eqIdx + 1).trim() + process.env[key] ??= value + } +} + +const isCI = Boolean(process.env.CI) + +export default defineConfig({ + testDir: './tests', + fullyParallel: false, + forbidOnly: isCI, + retries: 0, + workers: 1, + maxFailures: isCI ? 3 : 0, // Stop early in CI after 3 failures + reporter: isCI ? [['html', {open: 'never'}], ['list']] : [['list']], + timeout: 3 * 60 * 1000, // 3 minutes per test + globalTimeout: 15 * 60 * 1000, // 15 minutes total + + use: { + trace: isCI ? 'on' : 'off', + screenshot: isCI ? 'on' : 'off', + video: 'off', + }, +}) diff --git a/packages/e2e/project.json b/packages/e2e/project.json new file mode 100644 index 0000000000..d8ec4c24fe --- /dev/null +++ b/packages/e2e/project.json @@ -0,0 +1,39 @@ +{ + "name": "e2e", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/e2e", + "projectType": "library", + "implicitDependencies": ["cli", "create-app"], + "tags": ["scope:e2e"], + "targets": { + "test": { + "executor": "nx:run-commands", + "dependsOn": ["^build"], + "options": { + "command": "pnpm playwright test", + "cwd": "packages/e2e" + } + }, + "lint": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm eslint \"**/*.ts\"", + "cwd": "packages/e2e" + } + }, + "lint:fix": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm eslint '**/*.ts' --fix", + "cwd": "packages/e2e" + } + }, + "type-check": { + "executor": "nx:run-commands", + "options": { + "command": "pnpm tsc --noEmit", + "cwd": "packages/e2e" + } + } + } +} diff --git a/packages/e2e/setup/cli.ts b/packages/e2e/setup/cli.ts new file mode 100644 index 0000000000..1f39f38ca6 --- /dev/null +++ b/packages/e2e/setup/cli.ts @@ -0,0 +1,242 @@ +/* eslint-disable no-console */ +import {envFixture, executables} from './env.js' +import {stripAnsi} from '../helpers/strip-ansi.js' +import {execa, type Options as ExecaOptions} from 'execa' +import type {E2EEnv} from './env.js' +import type * as pty from 'node-pty' + +export interface ExecResult { + stdout: string + stderr: string + exitCode: number +} + +export interface SpawnedProcess { + /** Wait for a string to appear in the PTY output */ + waitForOutput(text: string, timeoutMs?: number): Promise + /** Send a single key to the PTY */ + sendKey(key: string): void + /** Send a line of text followed by Enter */ + sendLine(line: string): void + /** Wait for the process to exit */ + waitForExit(timeoutMs?: number): Promise + /** Kill the process */ + kill(): void + /** Get all output captured so far (ANSI stripped) */ + getOutput(): string + /** The underlying node-pty process */ + readonly ptyProcess: pty.IPty +} + +export interface CLIProcess { + /** Execute a CLI command non-interactively via execa */ + exec(args: string[], opts?: {cwd?: string; env?: NodeJS.ProcessEnv; timeout?: number}): Promise + /** Execute the create-app binary non-interactively via execa */ + execCreateApp(args: string[], opts?: {cwd?: string; env?: NodeJS.ProcessEnv; timeout?: number}): Promise + /** Spawn an interactive CLI command via node-pty */ + spawn(args: string[], opts?: {cwd?: string; env?: NodeJS.ProcessEnv}): Promise +} + +/** + * Test-scoped fixture providing CLI process management. + * Tracks all spawned processes and kills them in teardown. + */ +export const cliFixture = envFixture.extend<{cli: CLIProcess}>({ + cli: async ({env}, use) => { + const spawnedProcesses: SpawnedProcess[] = [] + + const cli: CLIProcess = { + async exec(args, opts = {}) { + // 3 min default + const timeout = opts.timeout ?? 3 * 60 * 1000 + const execaOpts: ExecaOptions = { + cwd: opts.cwd, + env: {...env.processEnv, ...opts.env}, + timeout, + reject: false, + } + + if (process.env.DEBUG === '1') { + console.log(`[e2e] exec: node ${executables.cli} ${args.join(' ')}`) + } + + const result = await execa('node', [executables.cli, ...args], execaOpts) + + return { + stdout: result.stdout ?? '', + stderr: result.stderr ?? '', + exitCode: result.exitCode ?? 1, + } + }, + + async execCreateApp(args, opts = {}) { + // 5 min default for scaffolding + const timeout = opts.timeout ?? 5 * 60 * 1000 + const execaOpts: ExecaOptions = { + cwd: opts.cwd, + env: {...env.processEnv, ...opts.env}, + timeout, + reject: false, + } + + if (process.env.DEBUG === '1') { + console.log(`[e2e] exec: node ${executables.createApp} ${args.join(' ')}`) + } + + const result = await execa('node', [executables.createApp, ...args], execaOpts) + + return { + stdout: result.stdout ?? '', + stderr: result.stderr ?? '', + exitCode: result.exitCode ?? 1, + } + }, + + async spawn(args, opts = {}) { + // Dynamic import to avoid requiring node-pty for Phase 1 tests + const nodePty = await import('node-pty') + + const spawnEnv: {[key: string]: string} = {} + for (const [key, value] of Object.entries({...env.processEnv, ...opts.env})) { + if (value !== undefined) { + spawnEnv[key] = value + } + } + + if (process.env.DEBUG === '1') { + console.log(`[e2e] spawn: node ${executables.cli} ${args.join(' ')}`) + } + + const ptyProcess = nodePty.spawn('node', [executables.cli, ...args], { + name: 'xterm-color', + cols: 120, + rows: 30, + cwd: opts.cwd, + env: spawnEnv, + }) + + let output = '' + const outputWaiters: {text: string; resolve: () => void; reject: (err: Error) => void}[] = [] + + ptyProcess.onData((data: string) => { + output += data + if (process.env.DEBUG === '1') { + process.stdout.write(data) + } + + // Check if any waiters are satisfied (check both raw and stripped output) + const stripped = stripAnsi(output) + for (let idx = outputWaiters.length - 1; idx >= 0; idx--) { + const waiter = outputWaiters[idx] + if (waiter && (stripped.includes(waiter.text) || output.includes(waiter.text))) { + waiter.resolve() + outputWaiters.splice(idx, 1) + } + } + }) + + let exitCode: number | undefined + let exitResolve: ((code: number) => void) | undefined + + ptyProcess.onExit(({exitCode: code}) => { + exitCode = code + if (exitResolve) { + exitResolve(code) + } + // Reject any remaining output waiters + for (const waiter of outputWaiters) { + waiter.reject(new Error(`Process exited (code ${code}) while waiting for output: "${waiter.text}"`)) + } + outputWaiters.length = 0 + }) + + const spawned: SpawnedProcess = { + ptyProcess, + + waitForOutput(text: string, timeoutMs = 3 * 60 * 1000) { + // Check if already in output (raw or stripped) + if (stripAnsi(output).includes(text) || output.includes(text)) { + return Promise.resolve() + } + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + const waiterIdx = outputWaiters.findIndex((waiter) => waiter.text === text) + if (waiterIdx >= 0) outputWaiters.splice(waiterIdx, 1) + reject( + new Error( + `Timed out after ${timeoutMs}ms waiting for output: "${text}"\n\nCaptured output:\n${stripAnsi( + output, + )}`, + ), + ) + }, timeoutMs) + + outputWaiters.push({ + text, + resolve: () => { + clearTimeout(timer) + resolve() + }, + reject: (err) => { + clearTimeout(timer) + reject(err) + }, + }) + }) + }, + + sendKey(key: string) { + ptyProcess.write(key) + }, + + sendLine(line: string) { + ptyProcess.write(`${line}\r`) + }, + + waitForExit(timeoutMs = 60 * 1000) { + if (exitCode !== undefined) { + return Promise.resolve(exitCode) + } + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + reject(new Error(`Timed out after ${timeoutMs}ms waiting for process exit`)) + }, timeoutMs) + + exitResolve = (code) => { + clearTimeout(timer) + resolve(code) + } + }) + }, + + kill() { + try { + ptyProcess.kill() + // eslint-disable-next-line no-catch-all/no-catch-all + } catch (_error) { + // Process may already be dead + } + }, + + getOutput() { + return stripAnsi(output) + }, + } + + spawnedProcesses.push(spawned) + return spawned + }, + } + + await use(cli) + + // Teardown: kill all spawned processes + for (const proc of spawnedProcesses) { + proc.kill() + } + }, +}) + +export {type E2EEnv} diff --git a/packages/e2e/setup/env.ts b/packages/e2e/setup/env.ts new file mode 100644 index 0000000000..36e8d8e6c7 --- /dev/null +++ b/packages/e2e/setup/env.ts @@ -0,0 +1,138 @@ +/* eslint-disable no-restricted-imports */ +import {test as base} from '@playwright/test' +import * as path from 'path' +import * as fs from 'fs' +import {fileURLToPath} from 'url' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +export interface E2EEnv { + /** Partners token for API auth (empty string if not set) */ + partnersToken: string + /** Primary test app client ID (empty string if not set) */ + clientId: string + /** Dev store FQDN (e.g. cli-e2e-test.myshopify.com) */ + storeFqdn: string + /** Secondary app client ID for config link tests */ + secondaryClientId: string + /** Environment variables to pass to CLI processes */ + processEnv: NodeJS.ProcessEnv + /** Temporary directory root for this worker */ + tempDir: string +} + +export const directories = { + root: path.resolve(__dirname, '../../..'), + packages: { + cli: path.resolve(__dirname, '../../../packages/cli'), + app: path.resolve(__dirname, '../../../packages/app'), + cliKit: path.resolve(__dirname, '../../../packages/cli-kit'), + }, +} + +export const executables = { + cli: path.resolve(__dirname, '../../../packages/cli/bin/run.js'), + createApp: path.resolve(__dirname, '../../../packages/create-app/bin/run.js'), +} + +/** + * Creates an isolated temporary directory with XDG subdirectories and .npmrc. + * Returns the temp directory path and the env vars to pass to child processes. + */ +export function createIsolatedEnv(baseDir: string): {tempDir: string; xdgEnv: {[key: string]: string}} { + const tempDir = fs.mkdtempSync(path.join(baseDir, 'e2e-')) + + const xdgDirs = { + XDG_DATA_HOME: path.join(tempDir, 'XDG_DATA_HOME'), + XDG_CONFIG_HOME: path.join(tempDir, 'XDG_CONFIG_HOME'), + XDG_STATE_HOME: path.join(tempDir, 'XDG_STATE_HOME'), + XDG_CACHE_HOME: path.join(tempDir, 'XDG_CACHE_HOME'), + } + + for (const dir of Object.values(xdgDirs)) { + fs.mkdirSync(dir, {recursive: true}) + } + + // Write .npmrc to ensure package resolution works in CI + fs.writeFileSync(path.join(tempDir, '.npmrc'), '//registry.npmjs.org/') + + return {tempDir, xdgEnv: xdgDirs} +} + +/** + * Asserts that a required environment variable is set. + * Call this at the top of tests that need auth. + */ +export function requireEnv( + env: E2EEnv, + ...keys: (keyof Pick)[] +): void { + for (const key of keys) { + if (!env[key]) { + const envVarNames: {[key: string]: string} = { + partnersToken: 'SHOPIFY_CLI_PARTNERS_TOKEN', + clientId: 'SHOPIFY_FLAG_CLIENT_ID', + storeFqdn: 'E2E_STORE_FQDN', + secondaryClientId: 'E2E_SECONDARY_CLIENT_ID', + } + throw new Error(`${envVarNames[key]} environment variable is required for this test`) + } + } +} + +/** + * Worker-scoped fixture providing auth tokens and environment configuration. + * Auth tokens are optional — tests that need them should call requireEnv(). + */ +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export const envFixture = base.extend<{}, {env: E2EEnv}>({ + env: [ + // eslint-disable-next-line no-empty-pattern + async ({}, use) => { + const partnersToken = process.env.SHOPIFY_CLI_PARTNERS_TOKEN ?? '' + const clientId = process.env.SHOPIFY_FLAG_CLIENT_ID ?? '' + const storeFqdn = process.env.E2E_STORE_FQDN ?? '' + const secondaryClientId = process.env.E2E_SECONDARY_CLIENT_ID ?? '' + + const tmpBase = process.env.E2E_TEMP_DIR ?? path.join(directories.root, '.e2e-tmp') + fs.mkdirSync(tmpBase, {recursive: true}) + + const {tempDir, xdgEnv} = createIsolatedEnv(tmpBase) + + const processEnv: NodeJS.ProcessEnv = { + ...process.env, + ...xdgEnv, + SHOPIFY_RUN_AS_USER: '0', + NODE_OPTIONS: '', + // Prevent interactive prompts + CI: '1', + } + + if (partnersToken) { + processEnv.SHOPIFY_CLI_PARTNERS_TOKEN = partnersToken + } + if (clientId) { + processEnv.SHOPIFY_FLAG_CLIENT_ID = clientId + } + if (storeFqdn) { + processEnv.SHOPIFY_FLAG_STORE = storeFqdn + } + + const env: E2EEnv = { + partnersToken, + clientId, + storeFqdn, + secondaryClientId, + processEnv, + tempDir, + } + + await use(env) + + // Cleanup: remove temp directory + fs.rmSync(tempDir, {recursive: true, force: true}) + }, + {scope: 'worker'}, + ], +}) diff --git a/packages/e2e/tests/smoke-pty.spec.ts b/packages/e2e/tests/smoke-pty.spec.ts new file mode 100644 index 0000000000..3a9f28676f --- /dev/null +++ b/packages/e2e/tests/smoke-pty.spec.ts @@ -0,0 +1,12 @@ +import {cliFixture as test} from '../setup/cli.js' +import {expect} from '@playwright/test' + +test.describe('PTY smoke test', () => { + test('shopify version runs via PTY', async ({cli}) => { + const proc = await cli.spawn(['version']) + await proc.waitForOutput('3.') + const code = await proc.waitForExit() + expect(code).toBe(0) + expect(proc.getOutput()).toMatch(/\d+\.\d+\.\d+/) + }) +}) diff --git a/packages/e2e/tests/smoke.spec.ts b/packages/e2e/tests/smoke.spec.ts new file mode 100644 index 0000000000..b64d20520d --- /dev/null +++ b/packages/e2e/tests/smoke.spec.ts @@ -0,0 +1,10 @@ +import {cliFixture as test} from '../setup/cli.js' +import {expect} from '@playwright/test' + +test.describe('Smoke test', () => { + test('shopify version runs successfully', async ({cli}) => { + const result = await cli.exec(['version']) + expect(result.exitCode).toBe(0) + expect(result.stdout).toMatch(/\d+\.\d+\.\d+/) + }) +}) diff --git a/packages/e2e/tsconfig.json b/packages/e2e/tsconfig.json new file mode 100644 index 0000000000..d87e9d3dea --- /dev/null +++ b/packages/e2e/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../configurations/tsconfig.json", + "include": ["./**/*.ts"], + "exclude": ["./dist", "./test-results", "./playwright-report", "./scripts"], + "compilerOptions": { + "outDir": "dist", + "composite": false, + "declaration": false, + "types": ["node"] + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a9a193f3f..100d053384 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -573,6 +573,27 @@ importers: specifier: ^2.1.1 version: 2.1.1(esbuild@0.27.2) + packages/e2e: + devDependencies: + '@playwright/test': + specifier: ^1.50.0 + version: 1.58.2 + '@types/node': + specifier: 18.19.70 + version: 18.19.70 + execa: + specifier: ^7.2.0 + version: 7.2.0 + node-pty: + specifier: ^1.0.0 + version: 1.1.0 + strip-ansi: + specifier: ^7.1.0 + version: 7.1.0 + tempy: + specifier: ^1.0.1 + version: 1.0.1 + packages/eslint-plugin-cli: dependencies: '@babel/core': @@ -3749,6 +3770,11 @@ packages: resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + '@playwright/test@1.58.2': + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} + hasBin: true + '@pnpm/config.env-replace@1.1.0': resolution: {integrity: sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==} engines: {node: '>=12.22.0'} @@ -5952,8 +5978,8 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.20.0: - resolution: {integrity: sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==} + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} enquirer@2.3.6: @@ -6571,6 +6597,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} @@ -7958,6 +7989,9 @@ packages: node-machine-id@1.1.12: resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==} + node-pty@1.1.0: + resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} + node-releases@2.0.21: resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} @@ -8419,6 +8453,16 @@ packages: resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} engines: {node: '>=10'} + playwright-core@1.58.2: + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.58.2: + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} + hasBin: true + pluralize@8.0.0: resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} engines: {node: '>=4'} @@ -9264,8 +9308,8 @@ packages: resolution: {integrity: sha512-0fk9zBqO67Nq5M/m45qHCJxylV/DhBlIOVExqgOMiCCrzrhU6tCibRXNqE3jwJLftzE9SNuZtYbpzcO+i9FiKw==} engines: {node: '>=14.16'} - strnum@2.2.0: - resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} + strnum@2.1.2: + resolution: {integrity: sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==} stubborn-fs@1.2.5: resolution: {integrity: sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g==} @@ -13485,7 +13529,7 @@ snapshots: dependencies: string-width: 5.1.2 string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.2 + strip-ansi: 7.1.0 strip-ansi-cjs: strip-ansi@6.0.1 wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 @@ -14221,6 +14265,10 @@ snapshots: '@pkgr/core@0.2.9': {} + '@playwright/test@1.58.2': + dependencies: + playwright: 1.58.2 + '@pnpm/config.env-replace@1.1.0': {} '@pnpm/network.ca-file@1.0.2': @@ -15312,8 +15360,8 @@ snapshots: '@typescript-eslint/project-service@8.43.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.43.0(typescript@5.9.3) - '@typescript-eslint/types': 8.43.0 + '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) + '@typescript-eslint/types': 8.56.1 debug: 4.4.0(supports-color@8.1.1) typescript: 5.9.3 transitivePeerDependencies: @@ -15385,7 +15433,7 @@ snapshots: is-glob: 4.0.3 minimatch: 9.0.6 semver: 7.6.3 - ts-api-utils: 2.1.0(typescript@5.9.3) + ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -15398,7 +15446,7 @@ snapshots: '@typescript-eslint/visitor-keys': 8.56.1 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.4 - semver: 7.7.3 + semver: 7.7.4 tinyglobby: 0.2.15 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 @@ -16850,7 +16898,7 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.20.0: + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -17144,7 +17192,7 @@ snapshots: eslint-plugin-es-x@7.8.0(eslint@9.39.3(jiti@2.4.2)): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.3(jiti@2.4.2)) - '@eslint-community/regexpp': 4.12.1 + '@eslint-community/regexpp': 4.12.2 eslint: 9.39.3(jiti@2.4.2) eslint-compat-utils: 0.5.1(eslint@9.39.3(jiti@2.4.2)) @@ -17187,7 +17235,7 @@ snapshots: hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 - minimatch: 3.1.2 + minimatch: 3.1.5 object.fromentries: 2.0.8 object.groupby: 1.0.3 object.values: 1.2.1 @@ -17254,7 +17302,7 @@ snapshots: eslint-plugin-n@17.24.0(eslint@9.39.3(jiti@2.4.2))(typescript@5.9.3): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.3(jiti@2.4.2)) - enhanced-resolve: 5.20.0 + enhanced-resolve: 5.19.0 eslint: 9.39.3(jiti@2.4.2) eslint-plugin-es-x: 7.8.0(eslint@9.39.3(jiti@2.4.2)) get-tsconfig: 4.13.6 @@ -17532,7 +17580,7 @@ snapshots: fast-xml-parser@5.3.6: dependencies: - strnum: 2.2.0 + strnum: 2.1.2 fastest-levenshtein@1.0.16: {} @@ -17732,6 +17780,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -18226,7 +18277,7 @@ snapshots: ink@5.0.1(@types/react@18.3.12)(react@18.3.1): dependencies: '@alcalzone/ansi-tokenize': 0.1.3 - ansi-escapes: 7.1.1 + ansi-escapes: 7.2.0 ansi-styles: 6.2.3 auto-bind: 5.0.1 chalk: 5.4.1 @@ -18919,7 +18970,7 @@ snapshots: ansi-escapes: 7.1.1 cli-cursor: 5.0.0 slice-ansi: 7.1.2 - strip-ansi: 7.1.2 + strip-ansi: 7.1.0 wrap-ansi: 9.0.2 loglevel@1.9.2: {} @@ -19210,8 +19261,7 @@ snapshots: node-abort-controller@3.1.1: {} - node-addon-api@7.1.1: - optional: true + node-addon-api@7.1.1: {} node-domexception@1.0.0: {} @@ -19231,6 +19281,10 @@ snapshots: node-machine-id@1.1.12: {} + node-pty@1.1.0: + dependencies: + node-addon-api: 7.1.1 + node-releases@2.0.21: {} node-stream-zip@1.15.0: {} @@ -19760,6 +19814,14 @@ snapshots: dependencies: find-up: 5.0.0 + playwright-core@1.58.2: {} + + playwright@1.58.2: + dependencies: + playwright-core: 1.58.2 + optionalDependencies: + fsevents: 2.3.2 + pluralize@8.0.0: {} possible-typed-array-names@1.1.0: {} @@ -20639,7 +20701,7 @@ snapshots: dependencies: eastasianwidth: 0.2.0 emoji-regex: 9.2.2 - strip-ansi: 7.1.2 + strip-ansi: 7.1.0 string-width@7.2.0: dependencies: @@ -20650,7 +20712,7 @@ snapshots: string-width@8.1.1: dependencies: get-east-asian-width: 1.4.0 - strip-ansi: 7.1.2 + strip-ansi: 7.1.0 string.prototype.includes@2.0.1: dependencies: @@ -20736,7 +20798,7 @@ snapshots: strip-json-comments@5.0.1: {} - strnum@2.2.0: {} + strnum@2.1.2: {} stubborn-fs@1.2.5: {} @@ -21623,7 +21685,7 @@ snapshots: dependencies: ansi-styles: 6.2.3 string-width: 5.1.2 - strip-ansi: 7.1.2 + strip-ansi: 7.1.0 wrap-ansi@9.0.2: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 421fedad98..e8596419cc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,7 @@ onlyBuiltDependencies: - '@parcel/watcher' - esbuild - msw + - node-pty - nx - protobufjs - yarn