Skip to content

Commit

Permalink
cherry-pick(#21828): feat(snapshots): use double-buffer to avoid whit…
Browse files Browse the repository at this point in the history
…e flash on hover
  • Loading branch information
dgozman committed Mar 21, 2023
1 parent e75fe01 commit 0646773
Show file tree
Hide file tree
Showing 6 changed files with 107 additions and 85 deletions.
16 changes: 15 additions & 1 deletion packages/trace-viewer/src/ui/snapshotTab.css
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,25 @@
box-shadow: 0 12px 28px 0 rgba(0,0,0,.2),0 2px 4px 0 rgba(0,0,0,.1);
}

iframe#snapshot {
.snapshot-switcher {
width: 100%;
height: calc(100% - var(--window-header-height));
position: relative;
}

iframe[name=snapshot] {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
background: white;
visibility: hidden;
}

iframe.snapshot-visible[name=snapshot] {
visibility: visible;
}

.no-snapshot {
Expand Down
83 changes: 63 additions & 20 deletions packages/trace-viewer/src/ui/snapshotTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,31 +77,61 @@ export const SnapshotTab: React.FunctionComponent<{
return { snapshots, snapshotInfoUrl, snapshotUrl, pointX, pointY, popoutUrl };
}, [snapshots, snapshotTab]);

const iframeRef = React.useRef<HTMLIFrameElement>(null);
const iframeRef0 = React.useRef<HTMLIFrameElement>(null);
const iframeRef1 = React.useRef<HTMLIFrameElement>(null);
const [snapshotInfo, setSnapshotInfo] = React.useState({ viewport: kDefaultViewport, url: '' });
const loadingRef = React.useRef({ iteration: 0, visibleIframe: 0 });

React.useEffect(() => {
(async () => {
const thisIteration = loadingRef.current.iteration + 1;
const newVisibleIframe = 1 - loadingRef.current.visibleIframe;
loadingRef.current.iteration = thisIteration;

const newSnapshotInfo = { url: '', viewport: kDefaultViewport };
if (snapshotInfoUrl) {
const response = await fetch(snapshotInfoUrl);
const info = await response.json();
if (!info.error)
setSnapshotInfo(info);
} else {
setSnapshotInfo({ viewport: kDefaultViewport, url: '' });
if (!info.error) {
newSnapshotInfo.url = info.url;
newSnapshotInfo.viewport = info.viewport;
}
}
if (!iframeRef.current)

// Interrupted by another load - bail out.
if (loadingRef.current.iteration !== thisIteration)
return;
try {
const newUrl = snapshotUrl + (pointX === undefined ? '' : `&pointX=${pointX}&pointY=${pointY}`);
// Try preventing history entry from being created.
if (iframeRef.current.contentWindow)
iframeRef.current.contentWindow.location.replace(newUrl);
else
iframeRef.current.src = newUrl;
} catch (e) {

const iframe = [iframeRef0, iframeRef1][newVisibleIframe].current;
if (iframe) {
let loadedCallback = () => {};
const loadedPromise = new Promise<void>(f => loadedCallback = f);
try {
iframe.addEventListener('load', loadedCallback);
iframe.addEventListener('error', loadedCallback);

const newUrl = snapshotUrl + (pointX === undefined ? '' : `&pointX=${pointX}&pointY=${pointY}`);
// Try preventing history entry from being created.
if (iframe.contentWindow)
iframe.contentWindow.location.replace(newUrl);
else
iframe.src = newUrl;

await loadedPromise;
} catch {
} finally {
iframe.removeEventListener('load', loadedCallback);
iframe.removeEventListener('error', loadedCallback);
}
}
// Interrupted by another load - bail out.
if (loadingRef.current.iteration !== thisIteration)
return;

loadingRef.current.visibleIframe = newVisibleIframe;
setSnapshotInfo(newSnapshotInfo);
})();
}, [iframeRef, snapshotUrl, snapshotInfoUrl, pointX, pointY]);
}, [snapshotUrl, snapshotInfoUrl, pointX, pointY]);

const windowHeaderHeight = 40;
const snapshotContainerSize = {
Expand Down Expand Up @@ -130,7 +160,14 @@ export const SnapshotTab: React.FunctionComponent<{
testIdAttributeName={testIdAttributeName}
highlightedLocator={highlightedLocator}
setHighlightedLocator={setHighlightedLocator}
iframe={iframeRef.current} />
iframe={iframeRef0.current} />
<InspectModeController
isInspecting={isInspecting}
sdkLanguage={sdkLanguage}
testIdAttributeName={testIdAttributeName}
highlightedLocator={highlightedLocator}
setHighlightedLocator={setHighlightedLocator}
iframe={iframeRef1.current} />
<Toolbar>
<ToolbarButton title='Pick locator' disabled={!popoutUrl} toggled={pickerVisible} onClick={() => {
setPickerVisible(!pickerVisible);
Expand Down Expand Up @@ -184,7 +221,10 @@ export const SnapshotTab: React.FunctionComponent<{
</div>
</div>
</div>
<iframe ref={iframeRef} id='snapshot' name='snapshot'></iframe>
<div className='snapshot-switcher'>
<iframe ref={iframeRef0} name='snapshot' className={loadingRef.current.visibleIframe === 0 ? 'snapshot-visible' : ''}></iframe>
<iframe ref={iframeRef1} name='snapshot' className={loadingRef.current.visibleIframe === 1 ? 'snapshot-visible' : ''}></iframe>
</div>
</div>
</div>
</div>;
Expand All @@ -210,14 +250,17 @@ export const InspectModeController: React.FunctionComponent<{
}> = ({ iframe, isInspecting, sdkLanguage, testIdAttributeName, highlightedLocator, setHighlightedLocator }) => {
React.useEffect(() => {
const win = iframe?.contentWindow as any;
let recorder: Recorder | undefined;
try {
if (!win || !isInspecting && !highlightedLocator && !win._recorder)
if (!win)
return;
recorder = win._recorder;
if (!recorder && !isInspecting && !highlightedLocator)
return;
} catch {
// Potential cross-origin exception.
// Potential cross-origin exception when accessing win._recorder.
return;
}
let recorder: Recorder | undefined = win._recorder;
if (!recorder) {
const injectedScript = new InjectedScript(win, false, sdkLanguage, testIdAttributeName, 1, 'chromium', []);
recorder = new Recorder(injectedScript, {
Expand Down
16 changes: 6 additions & 10 deletions tests/config/traceViewerFixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import type { Fixtures, Frame, Locator, Page, Browser, BrowserContext } from '@playwright/test';
import type { Fixtures, FrameLocator, Locator, Page, Browser, BrowserContext } from '@playwright/test';
import { showTraceViewer } from '../../packages/playwright-core/lib/server';

type BaseTestFixtures = {
Expand Down Expand Up @@ -51,7 +51,7 @@ class TraceViewerPage {
this.consoleStacks = page.locator('.console-stack');
this.stackFrames = page.getByTestId('stack-trace').locator('.list-view-entry');
this.networkRequests = page.locator('.network-request-title');
this.snapshotContainer = page.locator('.snapshot-container iframe');
this.snapshotContainer = page.locator('.snapshot-container iframe.snapshot-visible[name=snapshot]');
}

async actionIconsText(action: string) {
Expand Down Expand Up @@ -96,15 +96,11 @@ class TraceViewerPage {
return result.sort();
}

async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise<Frame> {
const existing = this.page.mainFrame().childFrames()[0];
await Promise.all([
existing ? existing.waitForNavigation() as any : Promise.resolve(),
this.selectAction(actionName, ordinal),
]);
while (this.page.frames().length < (hasSubframe ? 3 : 2))
async snapshotFrame(actionName: string, ordinal: number = 0, hasSubframe: boolean = false): Promise<FrameLocator> {
await this.selectAction(actionName, ordinal);
while (this.page.frames().length < (hasSubframe ? 4 : 3))
await this.page.waitForEvent('frameattached');
return this.page.mainFrame().childFrames()[0];
return this.page.frameLocator('iframe.snapshot-visible[name=snapshot]');
}
}

Expand Down
71 changes: 20 additions & 51 deletions tests/library/trace-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ test('should capture iframe with sandbox attribute', async ({ page, server, runA

// Render snapshot, check expectations.
const snapshotFrame = await traceViewer.snapshotFrame('page.evaluate', 0, true);
const button = await snapshotFrame.childFrames()[0].waitForSelector('button');
const button = snapshotFrame.frameLocator('iframe').locator('button');
expect(await button.textContent()).toBe('Hello iframe');
});

Expand All @@ -283,8 +283,8 @@ test('should capture data-url svg iframe', async ({ page, server, runAndTrace })

// Render snapshot, check expectations.
const snapshotFrame = await traceViewer.snapshotFrame('page.evaluate', 0, true);
await expect(snapshotFrame.childFrames()[0].locator('svg')).toBeVisible();
const content = await snapshotFrame.childFrames()[0].content();
await expect(snapshotFrame.frameLocator('iframe').locator('svg')).toBeVisible();
const content = await snapshotFrame.frameLocator('iframe').locator(':root').innerHTML();
expect(content).toContain(`d="M16.5 3c-1.74 0-3.41.81-4.5 2.09C10.91 3.81 9.24 3 7.5 3 4.42 3 2 5.42 2 8.5c0 3.78 3.4 6.86 8.55 11.54L12 21.35l1.45-1.32C18.6 15.36 22 12.28 22 8.5 22 5.42 19.58 3 16.5 3zm-4.4 15.55l-.1.1-.1-.1C7.14 14.24 4 11.39 4 8.5 4 6.5 5.5 5 7.5 5c1.54 0 3.04.99 3.57 2.36h1.87C13.46 5.99 14.96 5 16.5 5c2 0 3.5 1.5 3.5 3.5 0 2.89-3.14 5.74-7.9 10.05z"`);
});

Expand Down Expand Up @@ -313,19 +313,9 @@ test('should contain adopted style sheets', async ({ page, runAndTrace, browserN
});

const frame = await traceViewer.snapshotFrame('page.evaluate');
await frame.waitForSelector('button');
const buttonColor = await frame.$eval('button', button => {
return window.getComputedStyle(button).color;
});
expect(buttonColor).toBe('rgb(255, 0, 0)');
const divColor = await frame.$eval('div', div => {
return window.getComputedStyle(div).color;
});
expect(divColor).toBe('rgb(0, 0, 255)');
const spanColor = await frame.$eval('span', span => {
return window.getComputedStyle(span).color;
});
expect(spanColor).toBe('rgb(0, 0, 255)');
await expect(frame.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)');
await expect(frame.locator('div')).toHaveCSS('color', 'rgb(0, 0, 255)');
await expect(frame.locator('span')).toHaveCSS('color', 'rgb(0, 0, 255)');
});

test('should work with adopted style sheets and replace/replaceSync', async ({ page, runAndTrace, browserName }) => {
Expand All @@ -350,27 +340,15 @@ test('should work with adopted style sheets and replace/replaceSync', async ({ p

{
const frame = await traceViewer.snapshotFrame('page.evaluate', 0);
await frame.waitForSelector('button');
const buttonColor = await frame.$eval('button', button => {
return window.getComputedStyle(button).color;
});
expect(buttonColor).toBe('rgb(255, 0, 0)');
await expect(frame.locator('button')).toHaveCSS('color', 'rgb(255, 0, 0)');
}
{
const frame = await traceViewer.snapshotFrame('page.evaluate', 1);
await frame.waitForSelector('button');
const buttonColor = await frame.$eval('button', button => {
return window.getComputedStyle(button).color;
});
expect(buttonColor).toBe('rgb(0, 0, 255)');
await expect(frame.locator('button')).toHaveCSS('color', 'rgb(0, 0, 255)');
}
{
const frame = await traceViewer.snapshotFrame('page.evaluate', 2);
await frame.waitForSelector('button');
const buttonColor = await frame.$eval('button', button => {
return window.getComputedStyle(button).color;
});
expect(buttonColor).toBe('rgb(0, 255, 0)');
await expect(frame.locator('button')).toHaveCSS('color', 'rgb(0, 255, 0)');
}
});

Expand Down Expand Up @@ -402,8 +380,7 @@ test('should restore scroll positions', async ({ page, runAndTrace, browserName

// Render snapshot, check expectations.
const frame = await traceViewer.snapshotFrame('scrollIntoViewIfNeeded');
const div = await frame.waitForSelector('div');
expect(await div.evaluate(div => div.scrollTop)).toBe(136);
expect(await frame.locator('div').evaluate(div => div.scrollTop)).toBe(136);
});

test('should restore control values', async ({ page, runAndTrace }) => {
Expand Down Expand Up @@ -450,13 +427,10 @@ test('should restore control values', async ({ page, runAndTrace }) => {
await expect(textarea).toHaveText('old');
await expect(textarea).toHaveValue('hello');

expect(await frame.$eval('option >> nth=0', o => o.hasAttribute('selected'))).toBe(false);
expect(await frame.$eval('option >> nth=1', o => o.hasAttribute('selected'))).toBe(true);
expect(await frame.$eval('option >> nth=2', o => o.hasAttribute('selected'))).toBe(false);
expect(await frame.locator('select').evaluate(s => {
const options = [...(s as HTMLSelectElement).selectedOptions];
return options.map(option => option.value);
})).toEqual(['opt1', 'opt3']);
expect(await frame.locator('option >> nth=0').evaluate(o => o.hasAttribute('selected'))).toBe(false);
expect(await frame.locator('option >> nth=1').evaluate(o => o.hasAttribute('selected'))).toBe(true);
expect(await frame.locator('option >> nth=2').evaluate(o => o.hasAttribute('selected'))).toBe(false);
await expect(frame.locator('select')).toHaveValues(['opt1', 'opt3']);
});

test('should work with meta CSP', async ({ page, runAndTrace, browserName }) => {
Expand All @@ -479,9 +453,8 @@ test('should work with meta CSP', async ({ page, runAndTrace, browserName }) =>

// Render snapshot, check expectations.
const frame = await traceViewer.snapshotFrame('$eval');
await frame.waitForSelector('div');
// Should render shadow dom with post-processing script.
expect(await frame.textContent('span')).toBe('World');
await expect(frame.locator('span')).toHaveText('World');
});

test('should handle multiple headers', async ({ page, server, runAndTrace, browserName }) => {
Expand All @@ -497,9 +470,8 @@ test('should handle multiple headers', async ({ page, server, runAndTrace, brows
});

const frame = await traceViewer.snapshotFrame('setContent');
await frame.waitForSelector('div');
const padding = await frame.$eval('body', body => window.getComputedStyle(body).paddingLeft);
expect(padding).toBe('42px');
await frame.locator('div').waitFor();
await expect(frame.locator('body')).toHaveCSS('padding-left', '42px');
});

test('should handle src=blob', async ({ page, server, runAndTrace, browserName }) => {
Expand All @@ -521,14 +493,12 @@ test('should handle src=blob', async ({ page, server, runAndTrace, browserName }
});

const frame = await traceViewer.snapshotFrame('page.evaluate');
const img = await frame.waitForSelector('img');
const size = await img.evaluate(e => (e as HTMLImageElement).naturalWidth);
const size = await frame.locator('img').evaluate(e => (e as HTMLImageElement).naturalWidth);
expect(size).toBe(10);
});

test('should register custom elements', async ({ page, server, runAndTrace }) => {
const traceViewer = await runAndTrace(async () => {
page.on('console', console.log);
await page.goto(server.EMPTY_PAGE);
await page.evaluate(() => {
customElements.define('my-element', class extends HTMLElement {
Expand Down Expand Up @@ -767,8 +737,7 @@ test('should serve overridden request', async ({ page, runAndTrace, server }) =>
});
// Render snapshot, check expectations.
const snapshotFrame = await traceViewer.snapshotFrame('page.goto');
const color = await snapshotFrame.locator('body').evaluate(body => getComputedStyle(body).backgroundColor);
expect(color).toBe('rgb(255, 0, 0)');
await expect(snapshotFrame.locator('body')).toHaveCSS('background-color', 'rgb(255, 0, 0)');
});

test('should display waitForLoadState even if did not wait for it', async ({ runAndTrace, server, page }) => {
Expand Down Expand Up @@ -803,7 +772,7 @@ test('should pick locator', async ({ page, runAndTrace, server }) => {
});
const snapshot = await traceViewer.snapshotFrame('page.setContent');
await traceViewer.page.getByTitle('Pick locator').click();
await snapshot.click('button');
await snapshot.locator('button').click();
await expect(traceViewer.page.locator('.cm-wrapper')).toContainText(`getByRole('button', { name: 'Submit' })`);
});

Expand Down
4 changes: 2 additions & 2 deletions tests/playwright-test/ui-mode-test-progress.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ test('should update trace live', async ({ runUITest, server }) => {
onePromise.resolve();

await expect(
page.frameLocator('id=snapshot').locator('body'),
page.frameLocator('iframe.snapshot-visible[name=snapshot]').locator('body'),
'verify snapshot'
).toHaveText('One', { timeout: 15000 });
await expect(listItem).toHaveText([
Expand All @@ -99,7 +99,7 @@ test('should update trace live', async ({ runUITest, server }) => {
twoPromise.resolve();

await expect(
page.frameLocator('id=snapshot').locator('body'),
page.frameLocator('iframe.snapshot-visible[name=snapshot]').locator('body'),
'verify snapshot'
).toHaveText('Two');

Expand Down
2 changes: 1 addition & 1 deletion tests/playwright-test/ui-mode-trace.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ test('should show snapshots for sync assertions', async ({ runUITest, server })
], { timeout: 15000 });

await expect(
page.frameLocator('id=snapshot').locator('button'),
page.frameLocator('iframe.snapshot-visible[name=snapshot]').locator('button'),
'verify snapshot'
).toHaveText('Submit');
});

0 comments on commit 0646773

Please sign in to comment.