diff --git a/packages/playwright-ct-core/src/cliOverrides.ts b/packages/playwright-ct-core/src/cliOverrides.ts index d57014f636..0e4c328694 100644 --- a/packages/playwright-ct-core/src/cliOverrides.ts +++ b/packages/playwright-ct-core/src/cliOverrides.ts @@ -21,6 +21,7 @@ import { resolveDirs } from './viteUtils'; import { runDevServer } from './devServer'; import type { FullConfigInternal } from 'playwright/lib/common/config'; import { removeFolderAndLogToConsole } from 'playwright/lib/runner/testServer'; +import type { FullConfig } from 'playwright/types/test'; export async function clearCacheCommand(config: FullConfigInternal) { const dirs = await resolveDirs(config.configDir, config.config); @@ -34,6 +35,6 @@ export async function findRelatedTestFilesCommand(files: string[], config: Full return { testFiles: affectedTestFiles(files) }; } -export async function runDevServerCommand(config: FullConfigInternal) { +export async function runDevServerCommand(config: FullConfig) { return await runDevServer(config); } diff --git a/packages/playwright-ct-core/src/devServer.ts b/packages/playwright-ct-core/src/devServer.ts index 3e833fcd9c..2152a672b3 100644 --- a/packages/playwright-ct-core/src/devServer.ts +++ b/packages/playwright-ct-core/src/devServer.ts @@ -17,28 +17,26 @@ import fs from 'fs'; import path from 'path'; import { Watcher } from 'playwright/lib/fsWatcher'; -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'; +import type { FullConfig } from 'playwright/test'; -export async function runDevServer(config: FullConfigInternal): Promise<() => Promise> { - const { registerSourceFile, frameworkPluginFactory } = frameworkConfig(config.config); - const runner = new Runner(config); - await runner.loadAllTests(); +export async function runDevServer(config: FullConfig): Promise<() => Promise> { + const { registerSourceFile, frameworkPluginFactory } = frameworkConfig(config); const componentRegistry: ComponentRegistry = new Map(); await populateComponentsFromTests(componentRegistry); - const dirs = await resolveDirs(config.configDir, config.config); + const configDir = config.configFile ? path.dirname(config.configFile) : config.rootDir; + const dirs = await resolveDirs(configDir, config); if (!dirs) { // eslint-disable-next-line no-console console.log(`Template file playwright/index.html is missing.`); return async () => {}; } const registerSource = injectedSource + '\n' + await fs.promises.readFile(registerSourceFile, 'utf-8'); - const viteConfig = await createConfig(dirs, config.config, frameworkPluginFactory, false); + const viteConfig = await createConfig(dirs, config, frameworkPluginFactory, false); viteConfig.plugins.push({ name: 'playwright:component-index', @@ -57,8 +55,8 @@ export async function runDevServer(config: FullConfigInternal): Promise<() => Pr const projectDirs = new Set(); const projectOutputs = new Set(); for (const p of config.projects) { - projectDirs.add(p.project.testDir); - projectOutputs.add(p.project.outputDir); + projectDirs.add(p.testDir); + projectOutputs.add(p.outputDir); } const globalWatcher = new Watcher(async () => { diff --git a/packages/playwright/src/program.ts b/packages/playwright/src/program.ts index 4b5bea19d3..c9b5a885c3 100644 --- a/packages/playwright/src/program.ts +++ b/packages/playwright/src/program.ts @@ -95,13 +95,14 @@ function addDevServerCommand(program: Command) { 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) + const config = await loadConfigFromFileRestartIfNeeded(options.config); + if (!config) return; - const { config } = configInternal; - const implementation = (config as any)['@playwright/test']?.['cli']?.['dev-server']; + const implementation = (config.config as any)['@playwright/test']?.['cli']?.['dev-server']; if (implementation) { - await implementation(configInternal); + const runner = new Runner(config); + await runner.loadAllTests(); + await implementation(config.config); } else { console.log(`DevServer is not available in the package you are using. Did you mean to use component testing?`); gracefullyProcessExitDoNotHang(1); diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index 4765bf5089..d0370fcfc3 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -176,17 +176,16 @@ export class TestServerDispatcher implements TestServerInterface { async startDevServer(params: Parameters[0]): ReturnType { if (this._devServerHandle) return { status: 'failed', report: [] }; - const { reporter, report } = await this._collectingInternalReporter(); - const config = await this._loadConfigOrReportError(reporter); + const { config, report, reporter, status } = await this._innerListTests({}); if (!config) - return { status: 'failed', report }; + return { status, 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); + this._devServerHandle = await devServerCommand(config.config); return { status: 'passed', report }; } catch (e) { reporter.onError(serializeError(e)); @@ -237,13 +236,21 @@ export class TestServerDispatcher implements TestServerInterface { async listTests(params: Parameters[0]): ReturnType { let result: Awaited>; this._queue = this._queue.then(async () => { - result = await this._innerListTests(params); + const { config, report, status } = await this._innerListTests(params); + if (config) + await this._updateWatchedDirs(config); + result = { report, status }; }).catch(printInternalError); await this._queue; return result!; } - private async _innerListTests(params: Parameters[0]): ReturnType { + private async _innerListTests(params: Parameters[0]): Promise<{ + report: ReportEntry[], + reporter: InternalReporter, + status: reporterTypes.FullResult['status'], + config?: FullConfigInternal, + }> { const overrides: ConfigCLIOverrides = { repeatEach: 1, retries: 0, @@ -252,7 +259,7 @@ export class TestServerDispatcher implements TestServerInterface { const { reporter, report } = await this._collectingInternalReporter(); const config = await this._loadConfigOrReportError(reporter, overrides); if (!config) - return { report, status: 'failed' }; + return { report, reporter, status: 'failed' }; config.cliArgs = params.locations || []; config.cliGrep = params.grep; @@ -266,7 +273,10 @@ export class TestServerDispatcher implements TestServerInterface { const status = await taskRunner.run(testRun, 0); await reporter.onEnd({ status }); await reporter.onExit(); + return { config, report, reporter, status }; + } + private async _updateWatchedDirs(config: FullConfigInternal) { this._watchedProjectDirs = new Set(); this._ignoredProjectOutputs = new Set(); for (const p of config.projects) { @@ -281,11 +291,10 @@ export class TestServerDispatcher implements TestServerInterface { } if (this._watchTestDirs) - await this.updateWatcher(false); - return { report, status }; + await this._updateWatcher(false); } - private async updateWatcher(reportPending: boolean) { + private async _updateWatcher(reportPending: boolean) { await this._watcher.update([...this._watchedProjectDirs, ...this._watchedTestDependencies], [...this._ignoredProjectOutputs], reportPending); } @@ -358,7 +367,7 @@ export class TestServerDispatcher implements TestServerInterface { this._watchedTestDependencies.add(fileName); dependenciesForTestFile(fileName).forEach(file => this._watchedTestDependencies.add(file)); } - await this.updateWatcher(true); + await this._updateWatcher(true); } async findRelatedTestFiles(params: Parameters[0]): ReturnType { diff --git a/tests/playwright-test/playwright.ct-dev-server.spec.ts b/tests/playwright-test/playwright.ct-dev-server.spec.ts new file mode 100644 index 0000000000..91edc93740 --- /dev/null +++ b/tests/playwright-test/playwright.ct-dev-server.spec.ts @@ -0,0 +1,49 @@ +/** + * 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. + */ + +import { test, expect, playwrightCtConfigText } from './playwright-test-fixtures'; + +test.describe.configure({ mode: 'parallel' }); + +test('should run dev-server and use it for tests', async ({ writeFiles, runInlineTest, startCLICommand }) => { + await writeFiles({ + 'playwright.config.ts': playwrightCtConfigText, + 'playwright/index.html': ``, + 'playwright/index.ts': ``, + 'src/button.tsx': ` + export const Button = () => ; + `, + 'src/button.test.tsx': ` + import { test, expect } from '@playwright/experimental-ct-react'; + import { Button } from './button'; + + test('pass', async ({ mount }) => { + const component = await mount(); + await expect(component).toHaveText('Button', { timeout: 1 }); + }); + `, + }); + + const devServerProcess = await startCLICommand({}, 'dev-server'); + await devServerProcess.waitForOutput('Dev Server listening on'); + + const result = await runInlineTest({}, { workers: 1 }); + expect(result.exitCode).toBe(0); + expect(result.passed).toBe(1); + expect(result.output).toContain('Dev Server is already running at'); + + await devServerProcess.kill(); +});