diff --git a/package-lock.json b/package-lock.json index cb83454a07..f7f302c7f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8220,9 +8220,6 @@ "playwright-core": "1.44.0-next", "vite": "^5.0.13" }, - "bin": { - "playwright": "cli.js" - }, "engines": { "node": ">=16" } diff --git a/packages/playwright-ct-core/cli.js b/packages/playwright-ct-core/cli.js deleted file mode 100755 index 5db59b62cd..0000000000 --- a/packages/playwright-ct-core/cli.js +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env node -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -const { program } = require('./lib/program'); -program.parse(process.argv); diff --git a/packages/playwright-ct-core/index.js b/packages/playwright-ct-core/index.js index 9583273ac2..cda6e7ad55 100644 --- a/packages/playwright-ct-core/index.js +++ b/packages/playwright-ct-core/index.js @@ -16,7 +16,7 @@ const { test: baseTest, expect, devices, defineConfig: originalDefineConfig } = require('playwright/test'); const { fixtures } = require('./lib/mount'); -const { clearCacheCommand, findRelatedTestFilesCommand } = require('./lib/cliOverrides'); +const { clearCacheCommand, runDevServerCommand, findRelatedTestFilesCommand } = require('./lib/cliOverrides'); const { createPlugin } = require('./lib/vitePlugin'); const defineConfig = (...configs) => { @@ -31,6 +31,7 @@ const defineConfig = (...configs) => { ], cli: { 'clear-cache': clearCacheCommand, + 'dev-server': runDevServerCommand, 'find-related-test-files': findRelatedTestFilesCommand, }, } diff --git a/packages/playwright-ct-core/package.json b/packages/playwright-ct-core/package.json index f51c6f6372..0304886630 100644 --- a/packages/playwright-ct-core/package.json +++ b/packages/playwright-ct-core/package.json @@ -29,8 +29,5 @@ "playwright-core": "1.44.0-next", "vite": "^5.0.13", "playwright": "1.44.0-next" - }, - "bin": { - "playwright": "cli.js" } } diff --git a/packages/playwright-ct-core/src/cliOverrides.ts b/packages/playwright-ct-core/src/cliOverrides.ts index aaa456427c..ccf5cf62f9 100644 --- a/packages/playwright-ct-core/src/cliOverrides.ts +++ b/packages/playwright-ct-core/src/cliOverrides.ts @@ -20,6 +20,8 @@ import { affectedTestFiles, cacheDir } from 'playwright/lib/transform/compilatio import { buildBundle } from './vitePlugin'; import { resolveDirs } from './viteUtils'; import type { FullConfig, Suite } from 'playwright/types/testReporter'; +import { runDevServer } from './devServer'; +import type { FullConfigInternal } from 'playwright/lib/common/config'; export async function clearCacheCommand(config: FullConfig, configDir: string) { const dirs = await resolveDirs(configDir, config); @@ -32,3 +34,7 @@ export async function findRelatedTestFilesCommand(files: string[], config: Full await buildBundle(config, configDir, suite); return { testFiles: affectedTestFiles(files) }; } + +export async function runDevServerCommand(config: FullConfigInternal) { + return await runDevServer(config); +} diff --git a/packages/playwright-ct-core/src/devServer.ts b/packages/playwright-ct-core/src/devServer.ts index 46fc604188..2e1e7c76bf 100644 --- a/packages/playwright-ct-core/src/devServer.ts +++ b/packages/playwright-ct-core/src/devServer.ts @@ -17,18 +17,14 @@ import fs from 'fs'; import path from 'path'; import { Watcher } from 'playwright/lib/fsWatcher'; -import { loadConfigFromFileRestartIfNeeded } from 'playwright/lib/common/configLoader'; import { Runner } from 'playwright/lib/runner/runner'; import type { PluginContext } from 'rollup'; import { source as injectedSource } from './generated/indexSource'; import { createConfig, populateComponentsFromTests, resolveDirs, transformIndexFile, frameworkConfig } from './viteUtils'; import type { ComponentRegistry } from './viteUtils'; +import type { FullConfigInternal } from 'playwright/lib/common/config'; -export async function runDevServer(configFile: string) { - const config = await loadConfigFromFileRestartIfNeeded(configFile); - if (!config) - return; - +export async function runDevServer(config: FullConfigInternal): Promise<() => Promise> { const { registerSourceFile, frameworkPluginFactory } = frameworkConfig(config.config); const runner = new Runner(config); await runner.loadAllTests(); @@ -39,7 +35,7 @@ export async function runDevServer(configFile: string) { if (!dirs) { // eslint-disable-next-line no-console console.log(`Template file playwright/index.html is missing.`); - return; + return async () => {}; } const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8'); const viteConfig = await createConfig(dirs, config.config, frameworkPluginFactory, false); @@ -56,7 +52,7 @@ export async function runDevServer(configFile: string) { await devServer.listen(); const protocol = viteConfig.server.https ? 'https:' : 'http:'; // eslint-disable-next-line no-console - console.log(`Test Server listening on ${protocol}//${viteConfig.server.host || 'localhost'}:${viteConfig.server.port}`); + console.log(`Dev Server listening on ${protocol}//${viteConfig.server.host || 'localhost'}:${viteConfig.server.port}`); const projectDirs = new Set(); const projectOutputs = new Set(); @@ -85,4 +81,5 @@ export async function runDevServer(configFile: string) { devServer.moduleGraph.onFileChange(rootModule.file!); }); globalWatcher.update([...projectDirs], [...projectOutputs], false); + return () => Promise.all([devServer.close(), globalWatcher.close()]).then(() => {}); } diff --git a/packages/playwright-ct-core/src/program.ts b/packages/playwright-ct-core/src/program.ts index 49b7823f89..3014578d69 100644 --- a/packages/playwright-ct-core/src/program.ts +++ b/packages/playwright-ct-core/src/program.ts @@ -14,19 +14,4 @@ * limitations under the License. */ -import type { Command } from 'playwright-core/lib/utilsBundle'; - -import { program } from 'playwright/lib/program'; -import { runDevServer } from './devServer'; export { program } from 'playwright/lib/program'; - -function addDevServerCommand(program: Command) { - const command = program.command('dev-server', { hidden: true }); - command.description('start dev server'); - command.option('-c, --config ', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`); - command.action(options => { - runDevServer(options.config); - }); -} - -addDevServerCommand(program); diff --git a/packages/playwright-ct-core/src/vitePlugin.ts b/packages/playwright-ct-core/src/vitePlugin.ts index 26964e079b..075e4670f3 100644 --- a/packages/playwright-ct-core/src/vitePlugin.ts +++ b/packages/playwright-ct-core/src/vitePlugin.ts @@ -96,7 +96,7 @@ export async function buildBundle(config: FullConfig, configDir: string, suite: const url = new URL(`${protocol}//${endpoint.host}:${endpoint.port}`); if (await isURLAvailable(url, true)) { // eslint-disable-next-line no-console - console.log(`Test Server is already running at ${url.toString()}, using it.\n`); + console.log(`Dev Server is already running at ${url.toString()}, using it.\n`); process.env.PLAYWRIGHT_TEST_BASE_URL = url.toString(); return null; } diff --git a/packages/playwright/src/fsWatcher.ts b/packages/playwright/src/fsWatcher.ts index a4627beb88..749c11ab0b 100644 --- a/packages/playwright/src/fsWatcher.ts +++ b/packages/playwright/src/fsWatcher.ts @@ -63,6 +63,10 @@ export class Watcher { }); } + async close() { + await this._fsWatcher?.close(); + } + private _reportEventsIfAny() { if (this._collector.length) this._onChange(this._collector.slice()); diff --git a/packages/playwright/src/isomorphic/teleReceiver.ts b/packages/playwright/src/isomorphic/teleReceiver.ts index 5b23b4a7b3..dbeb2645a0 100644 --- a/packages/playwright/src/isomorphic/teleReceiver.ts +++ b/packages/playwright/src/isomorphic/teleReceiver.ts @@ -19,6 +19,8 @@ import type { Metadata } from '../../types/test'; import type * as reporterTypes from '../../types/testReporter'; import type { ReporterV2 } from '../reporters/reporterV2'; +// -- Reuse boundary -- Everything below this line is reused in the vscode extension. + export type StringIntern = (s: string) => string; export type JsonLocation = reporterTypes.Location; export type JsonError = string; diff --git a/packages/playwright/src/isomorphic/testServerConnection.ts b/packages/playwright/src/isomorphic/testServerConnection.ts index 36b4683d95..ab1810fd0e 100644 --- a/packages/playwright/src/isomorphic/testServerConnection.ts +++ b/packages/playwright/src/isomorphic/testServerConnection.ts @@ -17,6 +17,8 @@ import type { TestServerInterface, TestServerInterfaceEvents } from '@testIsomorphic/testServerInterface'; import * as events from './events'; +// -- Reuse boundary -- Everything below this line is reused in the vscode extension. + export class TestServerConnection implements TestServerInterface, TestServerInterfaceEvents { readonly onClose: events.Event; readonly onReport: events.Event; @@ -155,6 +157,14 @@ export class TestServerConnection implements TestServerInterface, TestServerInte return await this._sendMessage('runGlobalTeardown', params); } + async startDevServer(params: Parameters[0]): ReturnType { + return await this._sendMessage('startDevServer', params); + } + + async stopDevServer(params: Parameters[0]): ReturnType { + return await this._sendMessage('stopDevServer', params); + } + async listFiles(params: Parameters[0]): ReturnType { return await this._sendMessage('listFiles', params); } diff --git a/packages/playwright/src/isomorphic/testServerInterface.ts b/packages/playwright/src/isomorphic/testServerInterface.ts index 34571513b4..d17127ad04 100644 --- a/packages/playwright/src/isomorphic/testServerInterface.ts +++ b/packages/playwright/src/isomorphic/testServerInterface.ts @@ -18,6 +18,8 @@ import type * as reporterTypes from '../../types/testReporter'; import type { Event } from './events'; import type { JsonEvent } from './teleReceiver'; +// -- Reuse boundary -- Everything below this line is reused in the vscode extension. + export type ReportEntry = JsonEvent; export interface TestServerInterface { @@ -52,6 +54,16 @@ export interface TestServerInterface { status: reporterTypes.FullResult['status'] }>; + startDevServer(params: {}): Promise<{ + report: ReportEntry[]; + status: reporterTypes.FullResult['status'] + }>; + + stopDevServer(params: {}): Promise<{ + report: ReportEntry[]; + status: reporterTypes.FullResult['status'] + }>; + listFiles(params: { projects?: string[]; }): Promise<{ diff --git a/packages/playwright/src/isomorphic/testTree.ts b/packages/playwright/src/isomorphic/testTree.ts index 4a5cc55aee..7a9ddc3668 100644 --- a/packages/playwright/src/isomorphic/testTree.ts +++ b/packages/playwright/src/isomorphic/testTree.ts @@ -17,6 +17,8 @@ export type TestItemStatus = 'none' | 'running' | 'scheduled' | 'passed' | 'failed' | 'skipped'; import type * as reporterTypes from '../../types/testReporter'; +// -- Reuse boundary -- Everything below this line is reused in the vscode extension. + export type TreeItemBase = { kind: 'root' | 'group' | 'case' | 'test', id: string; diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 3c271b0763..0502dc1563 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -105,6 +105,25 @@ function addFindRelatedTestFilesCommand(program: Command) { }); } +function addDevServerCommand(program: Command) { + const command = program.command('dev-server', { hidden: true }); + command.description('start dev server'); + command.option('-c, --config ', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`); + command.action(async options => { + const configInternal = await loadConfigFromFileRestartIfNeeded(options.config); + if (!configInternal) + return; + const { config } = configInternal; + const implementation = (config as any)['@playwright/test']?.['cli']?.['dev-server']; + if (implementation) { + await implementation(configInternal); + } else { + console.log(`DevServer is not available in the package you are using. Did you mean to use component testing?`); + gracefullyProcessExitDoNotHang(1); + } + }); +} + function addTestServerCommand(program: Command) { const command = program.command('test-server', { hidden: true }); command.description('start test server'); @@ -362,4 +381,5 @@ addListFilesCommand(program); addMergeReportsCommand(program); addClearCacheCommand(program); addFindRelatedTestFilesCommand(program); +addDevServerCommand(program); addTestServerCommand(program); diff --git a/packages/playwright/src/reporters/teleEmitter.ts b/packages/playwright/src/reporters/teleEmitter.ts index c374c5abe9..59ad776a38 100644 --- a/packages/playwright/src/reporters/teleEmitter.ts +++ b/packages/playwright/src/reporters/teleEmitter.ts @@ -21,6 +21,8 @@ import type * as teleReceiver from '../isomorphic/teleReceiver'; import { serializeRegexPatterns } from '../isomorphic/teleReceiver'; import type { ReporterV2 } from './reporterV2'; +// -- Reuse boundary -- Everything below this line is reused in the vscode extension. + export type TeleReporterEmitterOptions = { omitOutput?: boolean; omitBuffers?: boolean; diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 7bfec4a583..762741d5ce 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -74,6 +74,7 @@ class TestServerDispatcher implements TestServerInterface { private _serializer = require.resolve('./uiModeReporter'); private _watchTestDirs = false; private _closeOnDisconnect = false; + private _devServerHandle: (() => Promise) | undefined; constructor(configFile: string | undefined) { this._configFile = configFile; @@ -172,6 +173,43 @@ class TestServerDispatcher implements TestServerInterface { return { status, report: globalSetup?.report || [] }; } + async startDevServer(params: Parameters[0]): ReturnType { + if (this._devServerHandle) + return { status: 'failed', report: [] }; + const { reporter, report } = await this._collectingReporter(); + const { config, error } = await this._loadConfig(this._configFile); + if (!config) { + reporter.onError(error!); + return { status: 'failed', report }; + } + const devServerCommand = (config.config as any)['@playwright/test']?.['cli']?.['dev-server']; + if (!devServerCommand) { + reporter.onError({ message: 'No dev-server command found in the configuration' }); + return { status: 'failed', report }; + } + try { + this._devServerHandle = await devServerCommand(config); + return { status: 'passed', report }; + } catch (e) { + reporter.onError(serializeError(e)); + return { status: 'failed', report }; + } + } + + async stopDevServer(params: Parameters[0]): ReturnType { + if (!this._devServerHandle) + return { status: 'failed', report: [] }; + try { + await this._devServerHandle(); + this._devServerHandle = undefined; + return { status: 'passed', report: [] }; + } catch (e) { + const { reporter, report } = await this._collectingReporter(); + reporter.onError(serializeError(e)); + return { status: 'failed', report }; + } + } + async listFiles(params: Parameters[0]): ReturnType { const { reporter, report } = await this._collectingReporter(); const { config, error } = await this._loadConfig(this._configFile);