diff --git a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts index 3de6bec1b828f..6b94c0cf485a9 100644 --- a/packages/playwright-core/src/server/trace/viewer/traceViewer.ts +++ b/packages/playwright-core/src/server/trace/viewer/traceViewer.ts @@ -105,7 +105,7 @@ export async function startTraceViewerServer(options?: TraceViewerServerOptions) return server; } -export async function installRootRedirect(server: HttpServer, traceUrls: string[], options: TraceViewerRedirectOptions) { +export async function installRootRedirect(server: HttpServer, traceUrls: string[], options: TraceViewerRedirectOptions & { resolvedConfigFile?: string }) { const params = new URLSearchParams(); for (const traceUrl of traceUrls) params.append('trace', traceUrl); @@ -115,6 +115,8 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[ params.append('isServer', ''); if (isUnderTest()) params.append('isUnderTest', 'true'); + if (options.resolvedConfigFile) + params.append('resolvedConfigFile', path.resolve(options.resolvedConfigFile)); for (const arg of options.args || []) params.append('arg', arg); if (options.grep) diff --git a/packages/playwright/src/common/configLoader.ts b/packages/playwright/src/common/configLoader.ts index 08a9c0a48d09f..de568114a1d0d 100644 --- a/packages/playwright/src/common/configLoader.ts +++ b/packages/playwright/src/common/configLoader.ts @@ -286,7 +286,16 @@ function validateProject(file: string, project: Project, title: string) { } } -export function resolveConfigFile(configFileOrDirectory: string): string | undefined { +export function resolveConfigLocation(configFile: string | undefined): ConfigLocation { + const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd(); + const resolvedConfigFile = resolveConfigFile(configFileOrDirectory); + return { + resolvedConfigFile, + configDir: resolvedConfigFile ? path.dirname(resolvedConfigFile) : configFileOrDirectory, + }; +} + +function resolveConfigFile(configFileOrDirectory: string): string | undefined { const resolveConfig = (configFile: string) => { if (fs.existsSync(configFile)) return configFile; @@ -309,22 +318,15 @@ export function resolveConfigFile(configFileOrDirectory: string): string | undef return configFile; // If there is no config, assume this as a root testing directory. return undefined; - } else { - // When passed a file, it must be a config file. - const configFile = resolveConfig(configFileOrDirectory); - return configFile!; } + // When passed a file, it must be a config file. + return configFileOrDirectory!; } export async function loadConfigFromFileRestartIfNeeded(configFile: string | undefined, overrides?: ConfigCLIOverrides, ignoreDeps?: boolean): Promise { - const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd(); - const resolvedConfigFile = resolveConfigFile(configFileOrDirectory); - if (restartWithExperimentalTsEsm(resolvedConfigFile)) + const location = resolveConfigLocation(configFile); + if (restartWithExperimentalTsEsm(location.resolvedConfigFile)) return null; - const location: ConfigLocation = { - configDir: resolvedConfigFile ? path.dirname(resolvedConfigFile) : configFileOrDirectory, - resolvedConfigFile, - }; return await loadConfig(location, overrides, ignoreDeps); } diff --git a/packages/playwright/src/runner/testServer.ts b/packages/playwright/src/runner/testServer.ts index da1584de967a3..c483d8c57ed0a 100644 --- a/packages/playwright/src/runner/testServer.ts +++ b/packages/playwright/src/runner/testServer.ts @@ -21,7 +21,7 @@ import { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'play import type { Transport, HttpServer } from 'playwright-core/lib/utils'; import type * as reporterTypes from '../../types/testReporter'; import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache'; -import type { FullConfigInternal } from '../common/config'; +import type { ConfigLocation, FullConfigInternal } from '../common/config'; import { InternalReporter } from '../reporters/internalReporter'; import { createReporterForTestServer, createReporters } from './reporters'; import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer, createTaskRunnerForWatchSetup, createTaskRunnerForListFiles } from './tasks'; @@ -33,7 +33,7 @@ import { Watcher } from '../fsWatcher'; import type { ReportEntry, TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface'; import { Runner } from './runner'; import type { ConfigCLIOverrides } from '../common/ipc'; -import { loadConfig, resolveConfigFile, restartWithExperimentalTsEsm } from '../common/configLoader'; +import { loadConfig, resolveConfigLocation, restartWithExperimentalTsEsm } from '../common/configLoader'; import { webServerPluginsForConfig } from '../plugins/webServerPlugin'; import type { TraceViewerRedirectOptions, TraceViewerServerOptions } from 'playwright-core/lib/server/trace/viewer/traceViewer'; import type { TestRunnerPluginRegistration } from '../plugins'; @@ -44,15 +44,15 @@ const originalStdoutWrite = process.stdout.write; const originalStderrWrite = process.stderr.write; class TestServer { - private _configFile: string | undefined; + private _configLocation: ConfigLocation; private _dispatcher: TestServerDispatcher | undefined; - constructor(configFile: string | undefined) { - this._configFile = configFile; + constructor(configLocation: ConfigLocation) { + this._configLocation = configLocation; } async start(options: { host?: string, port?: number }): Promise { - this._dispatcher = new TestServerDispatcher(this._configFile); + this._dispatcher = new TestServerDispatcher(this._configLocation); return await startTraceViewerServer({ ...options, transport: this._dispatcher.transport }); } @@ -63,7 +63,7 @@ class TestServer { } class TestServerDispatcher implements TestServerInterface { - private _configFile: string | undefined; + private _configLocation: ConfigLocation; private _globalWatcher: Watcher; private _testWatcher: Watcher; private _testRun: { run: Promise, stop: ManualPromise } | undefined; @@ -77,8 +77,8 @@ class TestServerDispatcher implements TestServerInterface { private _closeOnDisconnect = false; private _devServerHandle: (() => Promise) | undefined; - constructor(configFile: string | undefined) { - this._configFile = configFile; + constructor(configLocation: ConfigLocation) { + this._configLocation = configLocation; this.transport = { dispatch: (method, params) => (this as any)[method](params), onclose: () => { @@ -145,7 +145,7 @@ class TestServerDispatcher implements TestServerInterface { await this.runGlobalTeardown(); const { reporter, report } = await this._collectingReporter(); - const { config, error } = await this._loadConfig(this._configFile); + const { config, error } = await this._loadConfig(); if (!config) { reporter.onError(error!); return { status: 'failed', report }; @@ -178,7 +178,7 @@ class TestServerDispatcher implements TestServerInterface { if (this._devServerHandle) return { status: 'failed', report: [] }; const { reporter, report } = await this._collectingReporter(); - const { config, error } = await this._loadConfig(this._configFile); + const { config, error } = await this._loadConfig(); if (!config) { reporter.onError(error!); return { status: 'failed', report }; @@ -212,14 +212,14 @@ class TestServerDispatcher implements TestServerInterface { } async clearCache(params: Parameters[0]): ReturnType { - const { config } = await this._loadConfig(this._configFile); + const { config } = await this._loadConfig(); if (config) await clearCacheAndLogToConsole(config); } async listFiles(params: Parameters[0]): ReturnType { const { reporter, report } = await this._collectingReporter(); - const { config, error } = await this._loadConfig(this._configFile); + const { config, error } = await this._loadConfig(); if (!config) { reporter.onError(error!); return { status: 'failed', report }; @@ -250,7 +250,7 @@ class TestServerDispatcher implements TestServerInterface { retries: 0, }; const { reporter, report } = await this._collectingReporter(); - const { config, error } = await this._loadConfig(this._configFile, overrides); + const { config, error } = await this._loadConfig(overrides); if (!config) { reporter.onError(error!); return { report: [], status: 'failed' }; @@ -315,7 +315,7 @@ class TestServerDispatcher implements TestServerInterface { else process.env.PW_LIVE_TRACE_STACKS = undefined; - const { config, error } = await this._loadConfig(this._configFile, overrides); + const { config, error } = await this._loadConfig(overrides); if (!config) { const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e)); wireReporter.onError(error!); @@ -359,7 +359,7 @@ class TestServerDispatcher implements TestServerInterface { } async findRelatedTestFiles(params: Parameters[0]): ReturnType { - const { config, error } = await this._loadConfig(this._configFile); + const { config, error } = await this._loadConfig(); if (error) return { testFiles: [], errors: [error] }; const runner = new Runner(config!); @@ -393,11 +393,9 @@ class TestServerDispatcher implements TestServerInterface { gracefullyProcessExitDoNotHang(0); } - private async _loadConfig(configFile: string | undefined, overrides?: ConfigCLIOverrides): Promise<{ config: FullConfigInternal | null, error?: reporterTypes.TestError }> { - const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd(); - const resolvedConfigFile = resolveConfigFile(configFileOrDirectory); + private async _loadConfig(overrides?: ConfigCLIOverrides): Promise<{ config: FullConfigInternal | null, error?: reporterTypes.TestError }> { try { - const config = await loadConfig({ resolvedConfigFile, configDir: resolvedConfigFile === configFileOrDirectory ? path.dirname(resolvedConfigFile) : configFileOrDirectory }, overrides); + const config = await loadConfig(this._configLocation, overrides); // Preserve plugin instances between setup and build. if (!this._plugins) this._plugins = config.plugins || []; @@ -411,8 +409,8 @@ class TestServerDispatcher implements TestServerInterface { } export async function runUIMode(configFile: string | undefined, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise { - return await innerRunTestServer(configFile, options, async (server: HttpServer, cancelPromise: ManualPromise) => { - await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html' }); + return await innerRunTestServer(configFile, options, async (server: HttpServer, cancelPromise: ManualPromise, configLocation: ConfigLocation) => { + await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html', resolvedConfigFile: configLocation.resolvedConfigFile }); if (options.host !== undefined || options.port !== undefined) { await openTraceInBrowser(server.urlPrefix()); } else { @@ -434,17 +432,18 @@ export async function runTestServer(configFile: string | undefined, options: { h }); } -async function innerRunTestServer(configFile: string | undefined, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise) => Promise): Promise { +async function innerRunTestServer(configFile: string | undefined, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise, configLocation: ConfigLocation) => Promise): Promise { if (restartWithExperimentalTsEsm(undefined, true)) return 'restarted'; - const testServer = new TestServer(configFile); + const configLocation = resolveConfigLocation(configFile); + const testServer = new TestServer(configLocation); const cancelPromise = new ManualPromise(); const sigintWatcher = new SigIntWatcher(); process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0)); void sigintWatcher.promise().then(() => cancelPromise.resolve()); try { const server = await testServer.start(options); - await openUI(server, cancelPromise); + await openUI(server, cancelPromise, configLocation); await cancelPromise; } finally { await testServer.stop(); diff --git a/packages/trace-viewer/src/ui/uiModeFiltersView.tsx b/packages/trace-viewer/src/ui/uiModeFiltersView.tsx index a6cff6ecf8025..0964620511bc6 100644 --- a/packages/trace-viewer/src/ui/uiModeFiltersView.tsx +++ b/packages/trace-viewer/src/ui/uiModeFiltersView.tsx @@ -20,7 +20,6 @@ import '@web/third_party/vscode/codicon.css'; import { settings } from '@web/uiUtils'; import React from 'react'; import './uiModeFiltersView.css'; -import type { TestModel } from './uiModeModel'; export const FiltersView: React.FC<{ filterText: string; @@ -29,9 +28,9 @@ export const FiltersView: React.FC<{ setStatusFilters: (filters: Map) => void; projectFilters: Map; setProjectFilters: (filters: Map) => void; - testModel: TestModel | undefined, + resolvedConfigFile: string | undefined, runTests: () => void; -}> = ({ filterText, setFilterText, statusFilters, setStatusFilters, projectFilters, setProjectFilters, testModel, runTests }) => { +}> = ({ filterText, setFilterText, statusFilters, setStatusFilters, projectFilters, setProjectFilters, resolvedConfigFile, runTests }) => { const [expanded, setExpanded] = React.useState(false); const inputRef = React.useRef(null); React.useEffect(() => { @@ -80,9 +79,8 @@ export const FiltersView: React.FC<{ const copy = new Map(projectFilters); copy.set(projectName, !copy.get(projectName)); setProjectFilters(copy); - const configFile = testModel?.config?.configFile; - if (configFile) - settings.setObject(configFile + ':projects', [...copy.entries()].filter(([_, v]) => v).map(([k]) => k)); + if (resolvedConfigFile) + settings.setObject(resolvedConfigFile + ':projects', [...copy.entries()].filter(([_, v]) => v).map(([k]) => k)); }}/>
{projectName || 'untitled'}
diff --git a/packages/trace-viewer/src/ui/uiModeView.tsx b/packages/trace-viewer/src/ui/uiModeView.tsx index 03abb468ab6df..7daf2eb9680bf 100644 --- a/packages/trace-viewer/src/ui/uiModeView.tsx +++ b/packages/trace-viewer/src/ui/uiModeView.tsx @@ -53,6 +53,7 @@ const guid = searchParams.get('ws'); const wsURL = new URL(`../${guid}`, window.location.toString()); wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:'); const queryParams = { + resolvedConfigFile: searchParams.get('resolvedConfigFile') || undefined, args: searchParams.getAll('arg'), grep: searchParams.get('grep') || undefined, grepInvert: searchParams.get('grepInvert') || undefined, @@ -201,8 +202,8 @@ export const UIModeView: React.FC<{}> = ({ if (!testModel) return; - const { config, rootSuite } = testModel; - const selectedProjects = config.configFile ? settings.getObject(config.configFile + ':projects', undefined) : undefined; + const { rootSuite } = testModel; + const selectedProjects = queryParams.resolvedConfigFile ? settings.getObject(queryParams.resolvedConfigFile + ':projects', undefined) : undefined; const newFilter = new Map(projectFilters); for (const projectName of newFilter.keys()) { if (!rootSuite.suites.find(s => s.title === projectName)) @@ -411,7 +412,7 @@ export const UIModeView: React.FC<{}> = ({ setStatusFilters={setStatusFilters} projectFilters={projectFilters} setProjectFilters={setProjectFilters} - testModel={testModel} + resolvedConfigFile={queryParams.resolvedConfigFile} runTests={() => runTests('bounce-if-busy', visibleTestIds)} /> {!isRunningTest && !progress &&
Tests
}