diff --git a/packages/trace-viewer/src/ui/actionList.tsx b/packages/trace-viewer/src/ui/actionList.tsx index 167910f22d..8c5b19c683 100644 --- a/packages/trace-viewer/src/ui/actionList.tsx +++ b/packages/trace-viewer/src/ui/actionList.tsx @@ -43,6 +43,7 @@ export const ActionList: React.FC = ({ revealConsole = () => {}, }) => { return action.callId} selectedItem={selectedAction} diff --git a/packages/trace-viewer/src/ui/watchMode.tsx b/packages/trace-viewer/src/ui/watchMode.tsx index e7f3ada542..e0504d00e5 100644 --- a/packages/trace-viewer/src/ui/watchMode.tsx +++ b/packages/trace-viewer/src/ui/watchMode.tsx @@ -99,15 +99,6 @@ export const WatchModeView: React.FC<{}> = ({ setProgress(newProgress); }; - const outputDir = React.useMemo(() => { - let outputDir = ''; - for (const p of rootSuite.value?.suites || []) { - outputDir = p.project()?.outputDir || ''; - break; - } - return outputDir; - }, [rootSuite]); - const runTests = (testIds: string[]) => { // Clear test results. { @@ -130,6 +121,7 @@ export const WatchModeView: React.FC<{}> = ({ const isRunningTest = !!runningState; const result = selectedTest?.results[0]; + const outputDir = selectedTest ? outputDirForTestCase(selectedTest) : undefined; return
@@ -388,7 +380,7 @@ const TestList: React.FC<{ }; const TraceView: React.FC<{ - outputDir: string, + outputDir: string | undefined, testCase: TestCase | undefined, result: TestResult | undefined, }> = ({ outputDir, testCase, result }) => { @@ -412,6 +404,11 @@ const TraceView: React.FC<{ return; } + if (!outputDir) { + setModel(undefined); + return; + } + const traceLocation = `${outputDir}/${artifactsFolderName(result!.workerIndex)}/traces/${testCase?.id}.json`; // Start polling running test. pollTimer.current = setTimeout(async () => { @@ -535,6 +532,14 @@ const sendMessageNoReply = (method: string, params?: any) => { }); }; +const outputDirForTestCase = (testCase: TestCase): string | undefined => { + for (let suite: Suite | undefined = testCase.parent; suite; suite = suite.parent) { + if (suite.project()) + return suite.project()?.outputDir; + } + return undefined; +}; + const fileNameForTreeItem = (treeItem?: TreeItem): string | undefined => { return treeItem?.location.file; }; diff --git a/packages/web/src/components/codeMirrorWrapper.tsx b/packages/web/src/components/codeMirrorWrapper.tsx index 38558f1bf3..c333e6ee0e 100644 --- a/packages/web/src/components/codeMirrorWrapper.tsx +++ b/packages/web/src/components/codeMirrorWrapper.tsx @@ -53,7 +53,7 @@ export const CodeMirrorWrapper: React.FC = ({ }) => { const codemirrorElement = React.useRef(null); const [modulePromise] = React.useState>(import('./codeMirrorModule').then(m => m.default)); - const codemirrorRef = React.useRef<{ cm: CodeMirror.Editor, highlight: SourceHighlight[], widgets: CodeMirror.LineWidget[] } | null>(null); + const codemirrorRef = React.useRef<{ cm: CodeMirror.Editor, highlight?: SourceHighlight[], widgets?: CodeMirror.LineWidget[] } | null>(null); const [codemirror, setCodemirror] = React.useState(); React.useEffect(() => { @@ -92,7 +92,7 @@ export const CodeMirrorWrapper: React.FC = ({ lineNumbers, lineWrapping: wrapLines, }); - codemirrorRef.current = { cm, highlight: [], widgets: [] }; + codemirrorRef.current = { cm }; setCodemirror(cm); return cm; })(); @@ -108,43 +108,47 @@ export const CodeMirrorWrapper: React.FC = ({ codemirror.on('change', (codemirror as any)[listenerSymbol]); } + let valueChanged = false; if (codemirror.getValue() !== text) { codemirror.setValue(text); + valueChanged = true; if (focusOnChange) { codemirror.execCommand('selectAll'); codemirror.focus(); } } - // Line highlight. - for (const h of codemirrorRef.current!.highlight) - codemirror.removeLineClass(h.line - 1, 'wrap'); - for (const h of highlight || []) - codemirror.addLineClass(h.line - 1, 'wrap', `source-line-${h.type}`); - codemirrorRef.current!.highlight = highlight || []; + if (valueChanged || JSON.stringify(highlight) !== JSON.stringify(codemirrorRef.current!.highlight)) { + // Line highlight. + for (const h of codemirrorRef.current!.highlight || []) + codemirror.removeLineClass(h.line - 1, 'wrap'); + for (const h of highlight || []) + codemirror.addLineClass(h.line - 1, 'wrap', `source-line-${h.type}`); - // Error widgets. - for (const w of codemirrorRef.current!.widgets) - codemirror.removeLineWidget(w); - const widgets: CodeMirror.LineWidget[] = []; - for (const h of highlight || []) { - if (h.type !== 'error') - continue; + // Error widgets. + for (const w of codemirrorRef.current!.widgets || []) + codemirror.removeLineWidget(w); + const widgets: CodeMirror.LineWidget[] = []; + for (const h of highlight || []) { + if (h.type !== 'error') + continue; - const line = codemirrorRef.current?.cm.getLine(h.line - 1); - if (line) { - const underlineWidgetElement = document.createElement('div'); - underlineWidgetElement.className = 'source-line-error-underline'; - underlineWidgetElement.innerHTML = ' '.repeat(line.length || 1); - widgets.push(codemirror.addLineWidget(h.line, underlineWidgetElement, { above: true, coverGutter: false })); + const line = codemirrorRef.current?.cm.getLine(h.line - 1); + if (line) { + const underlineWidgetElement = document.createElement('div'); + underlineWidgetElement.className = 'source-line-error-underline'; + underlineWidgetElement.innerHTML = ' '.repeat(line.length || 1); + widgets.push(codemirror.addLineWidget(h.line, underlineWidgetElement, { above: true, coverGutter: false })); + } + + const errorWidgetElement = document.createElement('div'); + errorWidgetElement.innerHTML = ansi2htmlMarkup(h.message || ''); + errorWidgetElement.className = 'source-line-error-widget'; + widgets.push(codemirror.addLineWidget(h.line, errorWidgetElement, { above: true, coverGutter: false })); } - - const errorWidgetElement = document.createElement('div'); - errorWidgetElement.innerHTML = ansi2htmlMarkup(h.message || ''); - errorWidgetElement.className = 'source-line-error-widget'; - widgets.push(codemirror.addLineWidget(h.line, errorWidgetElement, { above: true, coverGutter: false })); + codemirrorRef.current!.highlight = highlight; + codemirrorRef.current!.widgets = widgets; } - codemirrorRef.current!.widgets = widgets; if (revealLine && codemirrorRef.current!.cm.lineCount() >= revealLine) codemirror.scrollIntoView({ line: revealLine - 1, ch: 0 }, 50); diff --git a/tests/playwright-test/ui-mode-test-progress.spec.ts b/tests/playwright-test/ui-mode-test-progress.spec.ts new file mode 100644 index 0000000000..5ce274c15a --- /dev/null +++ b/tests/playwright-test/ui-mode-test-progress.spec.ts @@ -0,0 +1,111 @@ +/** + * 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 { ManualPromise } from '../../packages/playwright-core/lib/utils/manualPromise'; +import { test, expect } from './ui-mode-fixtures'; +test.describe.configure({ mode: 'parallel' }); + +test('should update trace live', async ({ runUITest, server }) => { + const onePromise = new ManualPromise(); + + server.setRoute('/one.html', async (req, res) => { + await onePromise; + res.end('One'); + }); + + const twoPromise = new ManualPromise(); + server.setRoute('/two.html', async (req, res) => { + await twoPromise; + res.end('Two'); + }); + + const page = await runUITest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + test('live test', async ({ page }) => { + await page.goto('${server.PREFIX}/one.html'); + await page.goto('${server.PREFIX}/two.html'); + }); + `, + }); + + // Start test. + await page.getByText('live test').dblclick(); + + // It should halt on loading one.html. + const listItem = page.getByTestId('action-list').getByRole('listitem'); + await expect( + listItem, + 'action list' + ).toHaveText([ + /browserContext.newPage[\d.]+m?s/, + /page.gotohttp:\/\/localhost:\d+\/one.html/ + ]); + + await expect( + listItem.locator(':scope.selected'), + 'last action to be selected' + ).toHaveText(/page.goto/); + await expect( + listItem.locator(':scope.selected .codicon.codicon-loading'), + 'spinner' + ).toBeVisible(); + + await expect( + page.locator('.CodeMirror .source-line-running'), + 'check source tab', + ).toHaveText(/4 await page.goto\('http:\/\/localhost:\d+\/one.html/); + + // Unlock the navigation step. + onePromise.resolve(); + + await expect( + page.frameLocator('id=snapshot').locator('body'), + 'verify snapshot' + ).toHaveText('One'); + await expect(listItem).toHaveText([ + /browserContext.newPage[\d.]+m?s/, + /page.gotohttp:\/\/localhost:\d+\/one.html[\d.]+m?s/, + /page.gotohttp:\/\/localhost:\d+\/two.html/ + ]); + await expect( + listItem.locator(':scope.selected'), + 'last action to be selected' + ).toHaveText(/page.goto/); + await expect( + listItem.locator(':scope.selected .codicon.codicon-loading'), + 'spinner' + ).toBeVisible(); + + await expect( + page.locator('.CodeMirror .source-line-running'), + 'check source tab', + ).toHaveText(/5 await page.goto\('http:\/\/localhost:\d+\/two.html/); + + // Unlock the navigation step. + twoPromise.resolve(); + + await expect( + page.frameLocator('id=snapshot').locator('body'), + 'verify snapshot' + ).toHaveText('Two'); + + await expect(listItem).toHaveText([ + /browserContext.newPage[\d.]+m?s/, + /page.gotohttp:\/\/localhost:\d+\/one.html[\d.]+m?s/, + /page.gotohttp:\/\/localhost:\d+\/two.html[\d.]+m?s/ + ]); +});