chore: surface syntax error in ui mode (#22982)

Fixes https://github.com/microsoft/playwright/issues/22863
This commit is contained in:
Pavel Feldman 2023-05-12 14:23:22 -07:00 committed by GitHub
parent 9472f79d32
commit 083d13a13d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 73 additions and 36 deletions

View file

@ -15,11 +15,12 @@
*/ */
import path from 'path'; import path from 'path';
import util from 'util';
import type { TestError } from '../../reporter'; import type { TestError } from '../../reporter';
import { isWorkerProcess, setCurrentlyLoadingFileSuite } from './globals'; import { isWorkerProcess, setCurrentlyLoadingFileSuite } from './globals';
import { Suite } from './test'; import { Suite } from './test';
import { requireOrImport } from './transform'; import { requireOrImport } from './transform';
import { serializeError } from '../util'; import { filterStackTrace } from '../util';
import { startCollectingFileDeps, stopCollectingFileDeps } from './compilationCache'; import { startCollectingFileDeps, stopCollectingFileDeps } from './compilationCache';
export const defaultTimeout = 30000; export const defaultTimeout = 30000;
@ -44,7 +45,7 @@ export async function loadTestFile(file: string, rootDir: string, testErrors?: T
} catch (e) { } catch (e) {
if (!testErrors) if (!testErrors)
throw e; throw e;
testErrors.push(serializeError(e)); testErrors.push(serializeLoadError(file, e));
} finally { } finally {
stopCollectingFileDeps(file); stopCollectingFileDeps(file);
setCurrentlyLoadingFileSuite(undefined); setCurrentlyLoadingFileSuite(undefined);
@ -72,3 +73,18 @@ export async function loadTestFile(file: string, rootDir: string, testErrors?: T
return suite; return suite;
} }
function serializeLoadError(file: string, error: Error | any): TestError {
if (error instanceof Error) {
const result: TestError = filterStackTrace(error);
// Babel parse errors have location.
const loc = (error as any).loc;
result.location = loc ? {
file,
line: loc.line || 0,
column: loc.column || 0,
} : undefined;
return result;
}
return { value: util.inspect(error) };
}

View file

@ -134,7 +134,7 @@ export function resolveHook(filename: string, specifier: string): string | undef
} }
} }
export function transformHook(code: string, filename: string, moduleUrl?: string): string { export function transformHook(preloadedCode: string, filename: string, moduleUrl?: string): string {
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx'); const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx');
const hasPreprocessor = const hasPreprocessor =
process.env.PW_TEST_SOURCE_TRANSFORM && process.env.PW_TEST_SOURCE_TRANSFORM &&
@ -142,7 +142,7 @@ export function transformHook(code: string, filename: string, moduleUrl?: string
process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE.split(pathSeparator).some(f => filename.startsWith(f)); process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE.split(pathSeparator).some(f => filename.startsWith(f));
const pluginsPrologue = babelPlugins; const pluginsPrologue = babelPlugins;
const pluginsEpilogue = hasPreprocessor ? [[process.env.PW_TEST_SOURCE_TRANSFORM!]] as BabelPlugin[] : []; const pluginsEpilogue = hasPreprocessor ? [[process.env.PW_TEST_SOURCE_TRANSFORM!]] as BabelPlugin[] : [];
const hash = calculateHash(code, filename, !!moduleUrl, pluginsPrologue, pluginsEpilogue); const hash = calculateHash(preloadedCode, filename, !!moduleUrl, pluginsPrologue, pluginsEpilogue);
const { cachedCode, addToCache } = getFromCompilationCache(filename, hash, moduleUrl); const { cachedCode, addToCache } = getFromCompilationCache(filename, hash, moduleUrl);
if (cachedCode) if (cachedCode)
return cachedCode; return cachedCode;
@ -151,17 +151,11 @@ export function transformHook(code: string, filename: string, moduleUrl?: string
// Silence the annoying warning. // Silence the annoying warning.
process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true'; process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true';
try { const { babelTransform }: { babelTransform: BabelTransformFunction } = require('./babelBundle');
const { babelTransform }: { babelTransform: BabelTransformFunction } = require('./babelBundle'); const { code, map } = babelTransform(filename, isTypeScript, !!moduleUrl, pluginsPrologue, pluginsEpilogue);
const { code, map } = babelTransform(filename, isTypeScript, !!moduleUrl, pluginsPrologue, pluginsEpilogue); if (code)
if (code) addToCache!(code, map);
addToCache!(code, map); return code || '';
return code || '';
} catch (e) {
// Re-throw error with a playwright-test stack
// that could be filtered out.
throw new Error(e.message);
}
} }
function calculateHash(content: string, filePath: string, isModule: boolean, pluginsPrologue: BabelPlugin[], pluginsEpilogue: BabelPlugin[]): string { function calculateHash(content: string, filePath: string, isModule: boolean, pluginsPrologue: BabelPlugin[], pluginsEpilogue: BabelPlugin[]): string {

View file

@ -280,10 +280,12 @@ class ExpectMetaInfoProxyHandler implements ProxyHandler<any> {
jestError.stack = jestError.name + ': ' + newMessage + '\n' + stringifyStackFrames(stackFrames).join('\n'); jestError.stack = jestError.name + ': ' + newMessage + '\n' + stringifyStackFrames(stackFrames).join('\n');
} }
const serializerError = serializeError(jestError); const serializedError = serializeError(jestError);
step.complete({ error: serializerError }); // Serialized error has filtered stack trace.
jestError.stack = serializedError.stack;
step.complete({ error: serializedError });
if (this._info.isSoft) if (this._info.isSoft)
testInfo._failWithError(serializerError, false /* isHardError */); testInfo._failWithError(serializedError, false /* isHardError */);
else else
throw jestError; throw jestError;
}; };

View file

@ -70,7 +70,7 @@ export class Runner {
webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p })); webServerPluginsForConfig(config).forEach(p => config.plugins.push({ factory: p }));
const reporter = new InternalReporter(await createReporters(config, listOnly ? 'list' : 'run')); const reporter = new InternalReporter(await createReporters(config, listOnly ? 'list' : 'run'));
const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process') const taskRunner = listOnly ? createTaskRunnerForList(config, reporter, 'in-process', { failOnLoadErrors: true })
: createTaskRunner(config, reporter); : createTaskRunner(config, reporter);
const testRun = new TestRun(config, reporter); const testRun = new TestRun(config, reporter);

View file

@ -102,9 +102,9 @@ function addRunTasks(taskRunner: TaskRunner<TestRun>, config: FullConfigInternal
return taskRunner; return taskRunner;
} }
export function createTaskRunnerForList(config: FullConfigInternal, reporter: InternalReporter, mode: 'in-process' | 'out-of-process'): TaskRunner<TestRun> { export function createTaskRunnerForList(config: FullConfigInternal, reporter: InternalReporter, mode: 'in-process' | 'out-of-process', options: { failOnLoadErrors: boolean }): TaskRunner<TestRun> {
const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout); const taskRunner = new TaskRunner<TestRun>(reporter, config.config.globalTimeout);
taskRunner.addTask('load tests', createLoadTask(mode, { filterOnly: false, failOnLoadErrors: false })); taskRunner.addTask('load tests', createLoadTask(mode, { ...options, filterOnly: false }));
taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => { taskRunner.addTask('report begin', async ({ reporter, rootSuite }) => {
reporter.onBegin(config.config, rootSuite!); reporter.onBegin(config.config, rootSuite!);
return () => reporter.onEnd(); return () => reporter.onEnd();
@ -172,7 +172,7 @@ function createLoadTask(mode: 'out-of-process' | 'in-process', options: { filter
await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors); await loadFileSuites(testRun, mode, options.failOnLoadErrors ? errors : softErrors);
testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly); testRun.rootSuite = await createRootSuite(testRun, options.failOnLoadErrors ? errors : softErrors, !!options.filterOnly);
// Fail when no tests. // Fail when no tests.
if (!testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard) if (options.failOnLoadErrors && !testRun.rootSuite.allTests().length && !testRun.config.cliPassWithNoTests && !testRun.config.config.shard)
throw new Error(`No tests found`); throw new Error(`No tests found`);
}; };
} }

View file

@ -149,7 +149,7 @@ class UIMode {
const reporter = new InternalReporter([listReporter]); const reporter = new InternalReporter([listReporter]);
this._config.cliListOnly = true; this._config.cliListOnly = true;
this._config.testIdMatcher = undefined; this._config.testIdMatcher = undefined;
const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process'); const taskRunner = createTaskRunnerForList(this._config, reporter, 'out-of-process', { failOnLoadErrors: false });
const testRun = new TestRun(this._config, reporter); const testRun = new TestRun(this._config, reporter);
clearCompilationCache(); clearCompilationCache();
reporter.onConfigure(this._config); reporter.onConfigure(this._config);

View file

@ -29,13 +29,15 @@ import type { RawStack } from 'playwright-core/lib/utils';
const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..'); const PLAYWRIGHT_TEST_PATH = path.join(__dirname, '..');
const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json')); const PLAYWRIGHT_CORE_PATH = path.dirname(require.resolve('playwright-core/package.json'));
export function filterStackTrace(e: Error) { export function filterStackTrace(e: Error): { message: string, stack: string } {
if (process.env.PWDEBUGIMPL) if (process.env.PWDEBUGIMPL)
return; return { message: e.message, stack: e.stack || '' };
const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || [])); const stackLines = stringifyStackFrames(filteredStackTrace(e.stack?.split('\n') || []));
const message = e.message; return {
e.stack = `${e.name}: ${e.message}\n${stackLines.join('\n')}`; message: e.message,
e.message = message; stack: `${e.name}: ${e.message}\n${stackLines.join('\n')}`
};
} }
export function filteredStackTrace(rawStack: RawStack): StackFrame[] { export function filteredStackTrace(rawStack: RawStack): StackFrame[] {
@ -65,13 +67,8 @@ export function stringifyStackFrames(frames: StackFrame[]): string[] {
} }
export function serializeError(error: Error | any): TestInfoError { export function serializeError(error: Error | any): TestInfoError {
if (error instanceof Error) { if (error instanceof Error)
filterStackTrace(error); return filterStackTrace(error);
return {
message: error.message,
stack: error.stack
};
}
return { return {
value: util.inspect(error) value: util.inspect(error)
}; };

View file

@ -65,7 +65,7 @@ test('should show selected test in sources', async ({ runUITest }) => {
).toHaveText(`3 test('third', () => {});`); ).toHaveText(`3 test('third', () => {});`);
}); });
test('should show syntax errors in file', async ({ runUITest }) => { test('should show top-level errors in file', async ({ runUITest }) => {
const { page } = await runUITest({ const { page } = await runUITest({
'a.test.ts': ` 'a.test.ts': `
import { test } from '@playwright/test'; import { test } from '@playwright/test';
@ -100,3 +100,31 @@ test('should show syntax errors in file', async ({ runUITest }) => {
'Assignment to constant variable.' 'Assignment to constant variable.'
]); ]);
}); });
test('should show syntax errors in file', async ({ runUITest }) => {
const { page } = await runUITest({
'a.test.ts': `
import { test } from '@playwright/test'&
test('first', () => {});
test('second', () => {});
`,
});
await expect.poll(dumpTestTree(page)).toBe(`
a.test.ts
`);
await page.getByTestId('test-tree').getByText('a.test.ts').click();
await expect(
page.getByTestId('source-code').locator('.source-tab-file-name')
).toHaveText('a.test.ts');
await expect(
page.locator('.CodeMirror .source-line-running'),
).toHaveText(`2 import { test } from '@playwright/test'&`);
await expect(
page.locator('.CodeMirror-linewidget')
).toHaveText([
'                                              ',
/Missing semicolon./
]);
});