();
@@ -153,8 +154,7 @@ export const AttachmentsTab: React.FunctionComponent<{
return
isEqualAttachment(a, selected)) ?? false}
- reveal={!!revealedAttachment && isEqualAttachment(a, revealedAttachment)}
+ reveal={(!!revealedAttachment && isEqualAttachment(a, revealedAttachment[0])) ? revealedAttachment : undefined}
/>
;
})}
diff --git a/packages/trace-viewer/src/ui/workbench.tsx b/packages/trace-viewer/src/ui/workbench.tsx
index c6fcd7e54c305..de59892772acb 100644
--- a/packages/trace-viewer/src/ui/workbench.tsx
+++ b/packages/trace-viewer/src/ui/workbench.tsx
@@ -59,7 +59,7 @@ export const Workbench: React.FunctionComponent<{
}> = ({ model, showSourcesFirst, rootDir, fallbackLocation, isLive, hideTimeline, status, annotations, inert, onOpenExternally, revealSource }) => {
const [selectedCallId, setSelectedCallId] = React.useState(undefined);
const [revealedError, setRevealedError] = React.useState(undefined);
- const [revealedAttachment, setRevealedAttachment] = React.useState(undefined);
+ const [revealedAttachment, setRevealedAttachment] = React.useState<[attachment: AfterActionTraceEventAttachment, renderCounter: number] | undefined>(undefined);
const [highlightedCallId, setHighlightedCallId] = React.useState();
const [highlightedEntry, setHighlightedEntry] = React.useState();
const [highlightedConsoleMessage, setHighlightedConsoleMessage] = React.useState();
@@ -148,7 +148,12 @@ export const Workbench: React.FunctionComponent<{
const revealAttachment = React.useCallback((attachment: AfterActionTraceEventAttachment) => {
selectPropertiesTab('attachments');
- setRevealedAttachment(attachment);
+ setRevealedAttachment(currentValue => {
+ if (!currentValue)
+ return [attachment, 0];
+ const revealCounter = currentValue[1];
+ return [attachment, revealCounter + 1];
+ });
}, [selectPropertiesTab]);
React.useEffect(() => {
@@ -238,7 +243,7 @@ export const Workbench: React.FunctionComponent<{
id: 'attachments',
title: 'Attachments',
count: attachments.length,
- render: () =>
+ render: () =>
};
const tabs: TabbedPaneTabModel[] = [
diff --git a/packages/web/src/uiUtils.ts b/packages/web/src/uiUtils.ts
index 3544ec4bdcf75..a0b7a59c36182 100644
--- a/packages/web/src/uiUtils.ts
+++ b/packages/web/src/uiUtils.ts
@@ -14,6 +14,7 @@
limitations under the License.
*/
+import type { EffectCallback } from 'react';
import React from 'react';
// Recalculates the value when dependencies change.
@@ -224,3 +225,26 @@ export function scrollIntoViewIfNeeded(element: Element | undefined) {
const kControlCodesRe = '\\u0000-\\u0020\\u007f-\\u009f';
export const kWebLinkRe = new RegExp('(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\\/\\/|www\\.)[^\\s' + kControlCodesRe + '"]{2,}[^\\s' + kControlCodesRe + '"\')}\\],:;.!?]', 'ug');
+
+/**
+ * Manages flash animation state.
+ * Calling `trigger` will turn `flash` to true for a second, and then back to false.
+ * If `trigger` is called while a flash is ongoing, the ongoing flash will be cancelled and after 50ms a new flash is started.
+ * @returns [flash, trigger]
+ */
+export function useFlash(): [boolean, EffectCallback] {
+ const [flash, setFlash] = React.useState(false);
+ const trigger = React.useCallback(() => {
+ const timeouts: any[] = [];
+ setFlash(currentlyFlashing => {
+ timeouts.push(setTimeout(() => setFlash(false), 1000));
+ if (!currentlyFlashing)
+ return true;
+
+ timeouts.push(setTimeout(() => setFlash(true), 50));
+ return false;
+ });
+ return () => timeouts.forEach(clearTimeout);
+ }, [setFlash]);
+ return [flash, trigger];
+}
diff --git a/tests/playwright-test/reporter-html.spec.ts b/tests/playwright-test/reporter-html.spec.ts
index 2640cb61c9d7e..57ef76a7ca0fb 100644
--- a/tests/playwright-test/reporter-html.spec.ts
+++ b/tests/playwright-test/reporter-html.spec.ts
@@ -959,10 +959,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await showReport();
await page.getByRole('link', { name: 'passing' }).click();
- const attachment = page.getByTestId('attachments').getByText('foo-2', { exact: true });
+ const attachment = page.getByText('foo-2', { exact: true });
await expect(attachment).not.toBeInViewport();
- await page.getByLabel('attach "foo-2"').click();
- await page.getByTitle('see "foo-2"').click();
+ await page.getByLabel(`attach "foo-2"`).getByTitle('reveal attachment').click();
await expect(attachment).toBeInViewport();
await page.reload();
@@ -989,10 +988,9 @@ for (const useIntermediateMergeReport of [true, false] as const) {
await showReport();
await page.getByRole('link', { name: 'passing' }).click();
- const attachment = page.getByTestId('attachments').getByText('attachment', { exact: true });
+ const attachment = page.getByText('attachment', { exact: true });
await expect(attachment).not.toBeInViewport();
- await page.getByLabel('step').click();
- await page.getByTitle('see "attachment"').click();
+ await page.getByLabel('step').getByTitle('reveal attachment').click();
await expect(attachment).toBeInViewport();
});