chore: read config file location from url params in ui mode

Reference https://github.com/microsoft/playwright/issues/29768
This commit is contained in:
Yury Semikhatsky 2024-04-05 17:32:29 -07:00
parent 9e89f292bf
commit 72edf84c69
5 changed files with 49 additions and 47 deletions

View file

@ -105,7 +105,7 @@ export async function startTraceViewerServer(options?: TraceViewerServerOptions)
return server; 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(); const params = new URLSearchParams();
for (const traceUrl of traceUrls) for (const traceUrl of traceUrls)
params.append('trace', traceUrl); params.append('trace', traceUrl);
@ -115,6 +115,8 @@ export async function installRootRedirect(server: HttpServer, traceUrls: string[
params.append('isServer', ''); params.append('isServer', '');
if (isUnderTest()) if (isUnderTest())
params.append('isUnderTest', 'true'); params.append('isUnderTest', 'true');
if (options.resolvedConfigFile)
params.append('resolvedConfigFile', path.resolve(options.resolvedConfigFile));
for (const arg of options.args || []) for (const arg of options.args || [])
params.append('arg', arg); params.append('arg', arg);
if (options.grep) if (options.grep)

View file

@ -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) => { const resolveConfig = (configFile: string) => {
if (fs.existsSync(configFile)) if (fs.existsSync(configFile))
return configFile; return configFile;
@ -309,22 +318,15 @@ export function resolveConfigFile(configFileOrDirectory: string): string | undef
return configFile; return configFile;
// If there is no config, assume this as a root testing directory. // If there is no config, assume this as a root testing directory.
return undefined; 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<FullConfigInternal | null> { export async function loadConfigFromFileRestartIfNeeded(configFile: string | undefined, overrides?: ConfigCLIOverrides, ignoreDeps?: boolean): Promise<FullConfigInternal | null> {
const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd(); const location = resolveConfigLocation(configFile);
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory); if (restartWithExperimentalTsEsm(location.resolvedConfigFile))
if (restartWithExperimentalTsEsm(resolvedConfigFile))
return null; return null;
const location: ConfigLocation = {
configDir: resolvedConfigFile ? path.dirname(resolvedConfigFile) : configFileOrDirectory,
resolvedConfigFile,
};
return await loadConfig(location, overrides, ignoreDeps); return await loadConfig(location, overrides, ignoreDeps);
} }

View file

@ -21,7 +21,7 @@ import { ManualPromise, gracefullyProcessExitDoNotHang, isUnderTest } from 'play
import type { Transport, HttpServer } from 'playwright-core/lib/utils'; import type { Transport, HttpServer } from 'playwright-core/lib/utils';
import type * as reporterTypes from '../../types/testReporter'; import type * as reporterTypes from '../../types/testReporter';
import { collectAffectedTestFiles, dependenciesForTestFile } from '../transform/compilationCache'; 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 { InternalReporter } from '../reporters/internalReporter';
import { createReporterForTestServer, createReporters } from './reporters'; import { createReporterForTestServer, createReporters } from './reporters';
import { TestRun, createTaskRunnerForList, createTaskRunnerForTestServer, createTaskRunnerForWatchSetup, createTaskRunnerForListFiles } from './tasks'; 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 type { ReportEntry, TestServerInterface, TestServerInterfaceEventEmitters } from '../isomorphic/testServerInterface';
import { Runner } from './runner'; import { Runner } from './runner';
import type { ConfigCLIOverrides } from '../common/ipc'; 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 { webServerPluginsForConfig } from '../plugins/webServerPlugin';
import type { TraceViewerRedirectOptions, TraceViewerServerOptions } from 'playwright-core/lib/server/trace/viewer/traceViewer'; import type { TraceViewerRedirectOptions, TraceViewerServerOptions } from 'playwright-core/lib/server/trace/viewer/traceViewer';
import type { TestRunnerPluginRegistration } from '../plugins'; import type { TestRunnerPluginRegistration } from '../plugins';
@ -44,15 +44,15 @@ const originalStdoutWrite = process.stdout.write;
const originalStderrWrite = process.stderr.write; const originalStderrWrite = process.stderr.write;
class TestServer { class TestServer {
private _configFile: string | undefined; private _configLocation: ConfigLocation;
private _dispatcher: TestServerDispatcher | undefined; private _dispatcher: TestServerDispatcher | undefined;
constructor(configFile: string | undefined) { constructor(configLocation: ConfigLocation) {
this._configFile = configFile; this._configLocation = configLocation;
} }
async start(options: { host?: string, port?: number }): Promise<HttpServer> { async start(options: { host?: string, port?: number }): Promise<HttpServer> {
this._dispatcher = new TestServerDispatcher(this._configFile); this._dispatcher = new TestServerDispatcher(this._configLocation);
return await startTraceViewerServer({ ...options, transport: this._dispatcher.transport }); return await startTraceViewerServer({ ...options, transport: this._dispatcher.transport });
} }
@ -63,7 +63,7 @@ class TestServer {
} }
class TestServerDispatcher implements TestServerInterface { class TestServerDispatcher implements TestServerInterface {
private _configFile: string | undefined; private _configLocation: ConfigLocation;
private _globalWatcher: Watcher; private _globalWatcher: Watcher;
private _testWatcher: Watcher; private _testWatcher: Watcher;
private _testRun: { run: Promise<reporterTypes.FullResult['status']>, stop: ManualPromise<void> } | undefined; private _testRun: { run: Promise<reporterTypes.FullResult['status']>, stop: ManualPromise<void> } | undefined;
@ -77,8 +77,8 @@ class TestServerDispatcher implements TestServerInterface {
private _closeOnDisconnect = false; private _closeOnDisconnect = false;
private _devServerHandle: (() => Promise<void>) | undefined; private _devServerHandle: (() => Promise<void>) | undefined;
constructor(configFile: string | undefined) { constructor(configLocation: ConfigLocation) {
this._configFile = configFile; this._configLocation = configLocation;
this.transport = { this.transport = {
dispatch: (method, params) => (this as any)[method](params), dispatch: (method, params) => (this as any)[method](params),
onclose: () => { onclose: () => {
@ -145,7 +145,7 @@ class TestServerDispatcher implements TestServerInterface {
await this.runGlobalTeardown(); await this.runGlobalTeardown();
const { reporter, report } = await this._collectingReporter(); const { reporter, report } = await this._collectingReporter();
const { config, error } = await this._loadConfig(this._configFile); const { config, error } = await this._loadConfig();
if (!config) { if (!config) {
reporter.onError(error!); reporter.onError(error!);
return { status: 'failed', report }; return { status: 'failed', report };
@ -178,7 +178,7 @@ class TestServerDispatcher implements TestServerInterface {
if (this._devServerHandle) if (this._devServerHandle)
return { status: 'failed', report: [] }; return { status: 'failed', report: [] };
const { reporter, report } = await this._collectingReporter(); const { reporter, report } = await this._collectingReporter();
const { config, error } = await this._loadConfig(this._configFile); const { config, error } = await this._loadConfig();
if (!config) { if (!config) {
reporter.onError(error!); reporter.onError(error!);
return { status: 'failed', report }; return { status: 'failed', report };
@ -212,14 +212,14 @@ class TestServerDispatcher implements TestServerInterface {
} }
async clearCache(params: Parameters<TestServerInterface['clearCache']>[0]): ReturnType<TestServerInterface['clearCache']> { async clearCache(params: Parameters<TestServerInterface['clearCache']>[0]): ReturnType<TestServerInterface['clearCache']> {
const { config } = await this._loadConfig(this._configFile); const { config } = await this._loadConfig();
if (config) if (config)
await clearCacheAndLogToConsole(config); await clearCacheAndLogToConsole(config);
} }
async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> { async listFiles(params: Parameters<TestServerInterface['listFiles']>[0]): ReturnType<TestServerInterface['listFiles']> {
const { reporter, report } = await this._collectingReporter(); const { reporter, report } = await this._collectingReporter();
const { config, error } = await this._loadConfig(this._configFile); const { config, error } = await this._loadConfig();
if (!config) { if (!config) {
reporter.onError(error!); reporter.onError(error!);
return { status: 'failed', report }; return { status: 'failed', report };
@ -250,7 +250,7 @@ class TestServerDispatcher implements TestServerInterface {
retries: 0, retries: 0,
}; };
const { reporter, report } = await this._collectingReporter(); const { reporter, report } = await this._collectingReporter();
const { config, error } = await this._loadConfig(this._configFile, overrides); const { config, error } = await this._loadConfig(overrides);
if (!config) { if (!config) {
reporter.onError(error!); reporter.onError(error!);
return { report: [], status: 'failed' }; return { report: [], status: 'failed' };
@ -315,7 +315,7 @@ class TestServerDispatcher implements TestServerInterface {
else else
process.env.PW_LIVE_TRACE_STACKS = undefined; 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) { if (!config) {
const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e)); const wireReporter = await this._wireReporter(e => this._dispatchEvent('report', e));
wireReporter.onError(error!); wireReporter.onError(error!);
@ -359,7 +359,7 @@ class TestServerDispatcher implements TestServerInterface {
} }
async findRelatedTestFiles(params: Parameters<TestServerInterface['findRelatedTestFiles']>[0]): ReturnType<TestServerInterface['findRelatedTestFiles']> { async findRelatedTestFiles(params: Parameters<TestServerInterface['findRelatedTestFiles']>[0]): ReturnType<TestServerInterface['findRelatedTestFiles']> {
const { config, error } = await this._loadConfig(this._configFile); const { config, error } = await this._loadConfig();
if (error) if (error)
return { testFiles: [], errors: [error] }; return { testFiles: [], errors: [error] };
const runner = new Runner(config!); const runner = new Runner(config!);
@ -393,11 +393,9 @@ class TestServerDispatcher implements TestServerInterface {
gracefullyProcessExitDoNotHang(0); gracefullyProcessExitDoNotHang(0);
} }
private async _loadConfig(configFile: string | undefined, overrides?: ConfigCLIOverrides): Promise<{ config: FullConfigInternal | null, error?: reporterTypes.TestError }> { private async _loadConfig(overrides?: ConfigCLIOverrides): Promise<{ config: FullConfigInternal | null, error?: reporterTypes.TestError }> {
const configFileOrDirectory = configFile ? path.resolve(process.cwd(), configFile) : process.cwd();
const resolvedConfigFile = resolveConfigFile(configFileOrDirectory);
try { 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. // Preserve plugin instances between setup and build.
if (!this._plugins) if (!this._plugins)
this._plugins = config.plugins || []; this._plugins = config.plugins || [];
@ -411,8 +409,8 @@ class TestServerDispatcher implements TestServerInterface {
} }
export async function runUIMode(configFile: string | undefined, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise<reporterTypes.FullResult['status'] | 'restarted'> { export async function runUIMode(configFile: string | undefined, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise<reporterTypes.FullResult['status'] | 'restarted'> {
return await innerRunTestServer(configFile, options, async (server: HttpServer, cancelPromise: ManualPromise<void>) => { return await innerRunTestServer(configFile, options, async (server: HttpServer, cancelPromise: ManualPromise<void>, configLocation: ConfigLocation) => {
await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html' }); await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html', resolvedConfigFile: configLocation.resolvedConfigFile });
if (options.host !== undefined || options.port !== undefined) { if (options.host !== undefined || options.port !== undefined) {
await openTraceInBrowser(server.urlPrefix()); await openTraceInBrowser(server.urlPrefix());
} else { } 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<void>) => Promise<void>): Promise<reporterTypes.FullResult['status'] | 'restarted'> { async function innerRunTestServer(configFile: string | undefined, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>, configLocation: ConfigLocation) => Promise<void>): Promise<reporterTypes.FullResult['status'] | 'restarted'> {
if (restartWithExperimentalTsEsm(undefined, true)) if (restartWithExperimentalTsEsm(undefined, true))
return 'restarted'; return 'restarted';
const testServer = new TestServer(configFile); const configLocation = resolveConfigLocation(configFile);
const testServer = new TestServer(configLocation);
const cancelPromise = new ManualPromise<void>(); const cancelPromise = new ManualPromise<void>();
const sigintWatcher = new SigIntWatcher(); const sigintWatcher = new SigIntWatcher();
process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0)); process.stdin.on('close', () => gracefullyProcessExitDoNotHang(0));
void sigintWatcher.promise().then(() => cancelPromise.resolve()); void sigintWatcher.promise().then(() => cancelPromise.resolve());
try { try {
const server = await testServer.start(options); const server = await testServer.start(options);
await openUI(server, cancelPromise); await openUI(server, cancelPromise, configLocation);
await cancelPromise; await cancelPromise;
} finally { } finally {
await testServer.stop(); await testServer.stop();

View file

@ -20,7 +20,6 @@ import '@web/third_party/vscode/codicon.css';
import { settings } from '@web/uiUtils'; import { settings } from '@web/uiUtils';
import React from 'react'; import React from 'react';
import './uiModeFiltersView.css'; import './uiModeFiltersView.css';
import type { TestModel } from './uiModeModel';
export const FiltersView: React.FC<{ export const FiltersView: React.FC<{
filterText: string; filterText: string;
@ -29,9 +28,9 @@ export const FiltersView: React.FC<{
setStatusFilters: (filters: Map<string, boolean>) => void; setStatusFilters: (filters: Map<string, boolean>) => void;
projectFilters: Map<string, boolean>; projectFilters: Map<string, boolean>;
setProjectFilters: (filters: Map<string, boolean>) => void; setProjectFilters: (filters: Map<string, boolean>) => void;
testModel: TestModel | undefined, resolvedConfigFile: string | undefined,
runTests: () => void; 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 [expanded, setExpanded] = React.useState(false);
const inputRef = React.useRef<HTMLInputElement>(null); const inputRef = React.useRef<HTMLInputElement>(null);
React.useEffect(() => { React.useEffect(() => {
@ -80,9 +79,8 @@ export const FiltersView: React.FC<{
const copy = new Map(projectFilters); const copy = new Map(projectFilters);
copy.set(projectName, !copy.get(projectName)); copy.set(projectName, !copy.get(projectName));
setProjectFilters(copy); setProjectFilters(copy);
const configFile = testModel?.config?.configFile; if (resolvedConfigFile)
if (configFile) settings.setObject(resolvedConfigFile + ':projects', [...copy.entries()].filter(([_, v]) => v).map(([k]) => k));
settings.setObject(configFile + ':projects', [...copy.entries()].filter(([_, v]) => v).map(([k]) => k));
}}/> }}/>
<div>{projectName || 'untitled'}</div> <div>{projectName || 'untitled'}</div>
</label> </label>

View file

@ -53,6 +53,7 @@ const guid = searchParams.get('ws');
const wsURL = new URL(`../${guid}`, window.location.toString()); const wsURL = new URL(`../${guid}`, window.location.toString());
wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:'); wsURL.protocol = (window.location.protocol === 'https:' ? 'wss:' : 'ws:');
const queryParams = { const queryParams = {
resolvedConfigFile: searchParams.get('resolvedConfigFile') || undefined,
args: searchParams.getAll('arg'), args: searchParams.getAll('arg'),
grep: searchParams.get('grep') || undefined, grep: searchParams.get('grep') || undefined,
grepInvert: searchParams.get('grepInvert') || undefined, grepInvert: searchParams.get('grepInvert') || undefined,
@ -201,8 +202,8 @@ export const UIModeView: React.FC<{}> = ({
if (!testModel) if (!testModel)
return; return;
const { config, rootSuite } = testModel; const { rootSuite } = testModel;
const selectedProjects = config.configFile ? settings.getObject<string[] | undefined>(config.configFile + ':projects', undefined) : undefined; const selectedProjects = queryParams.resolvedConfigFile ? settings.getObject<string[] | undefined>(queryParams.resolvedConfigFile + ':projects', undefined) : undefined;
const newFilter = new Map(projectFilters); const newFilter = new Map(projectFilters);
for (const projectName of newFilter.keys()) { for (const projectName of newFilter.keys()) {
if (!rootSuite.suites.find(s => s.title === projectName)) if (!rootSuite.suites.find(s => s.title === projectName))
@ -411,7 +412,7 @@ export const UIModeView: React.FC<{}> = ({
setStatusFilters={setStatusFilters} setStatusFilters={setStatusFilters}
projectFilters={projectFilters} projectFilters={projectFilters}
setProjectFilters={setProjectFilters} setProjectFilters={setProjectFilters}
testModel={testModel} resolvedConfigFile={queryParams.resolvedConfigFile}
runTests={() => runTests('bounce-if-busy', visibleTestIds)} /> runTests={() => runTests('bounce-if-busy', visibleTestIds)} />
<Toolbar noMinHeight={true}> <Toolbar noMinHeight={true}>
{!isRunningTest && !progress && <div className='section-title'>Tests</div>} {!isRunningTest && !progress && <div className='section-title'>Tests</div>}