Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 284 additions & 0 deletions packages/react-devtools-shared/src/__tests__/profilerContext-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -655,4 +655,288 @@ describe('ProfilerContext', () => {

document.body.removeChild(profilerContainer);
});

it('should navigate between commits when the keyboard shortcut is pressed', async () => {
const Parent = () => <Child />;
const Child = () => null;

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
utils.act(() => root.render(<Parent />));

// Profile and record multiple commits
await utils.actAsync(() => store.profilerStore.startProfiling());
await utils.actAsync(() => root.render(<Parent />)); // Commit 1
await utils.actAsync(() => root.render(<Parent />)); // Commit 2
await utils.actAsync(() => root.render(<Parent />)); // Commit 3
await utils.actAsync(() => store.profilerStore.stopProfiling());

const Profiler =
require('react-devtools-shared/src/devtools/views/Profiler/Profiler').default;
const {
TimelineContextController,
} = require('react-devtools-timeline/src/TimelineContext');
const {
SettingsContextController,
} = require('react-devtools-shared/src/devtools/views/Settings/SettingsContext');
const {
ModalDialogContextController,
} = require('react-devtools-shared/src/devtools/views/ModalDialog');

let context: Context = ((null: any): Context);
function ContextReader() {
context = React.useContext(ProfilerContext);
return null;
}

const profilerContainer = document.createElement('div');
document.body.appendChild(profilerContainer);

const profilerRoot = ReactDOMClient.createRoot(profilerContainer);

await utils.actAsync(() => {
profilerRoot.render(
<Contexts>
<SettingsContextController browserTheme="light">
<ModalDialogContextController>
<TimelineContextController>
<Profiler />
<ContextReader />
</TimelineContextController>
</ModalDialogContextController>
</SettingsContextController>
</Contexts>,
);
});

// Verify we have profiling data with 3 commits
expect(context.didRecordCommits).toBe(true);
expect(context.profilingData).not.toBeNull();
const rootID = context.rootID;
expect(rootID).not.toBeNull();
const dataForRoot = context.profilingData.dataForRoots.get(rootID);
expect(dataForRoot.commitData.length).toBe(3);
// Should start at the first commit
expect(context.selectedCommitIndex).toBe(0);

const ownerWindow = profilerContainer.ownerDocument.defaultView;
const isMac =
typeof navigator !== 'undefined' &&
navigator.platform.toUpperCase().indexOf('MAC') >= 0;

// Test ArrowRight navigation (forward) with correct modifier
const arrowRightEvent = new KeyboardEvent('keydown', {
key: 'ArrowRight',
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
});

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(1);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(2);

// Test wrap-around (last -> first)
await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(0);

// Test ArrowLeft navigation (backward) with correct modifier
const arrowLeftEvent = new KeyboardEvent('keydown', {
key: 'ArrowLeft',
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
});

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowLeftEvent);
}, false);
expect(context.selectedCommitIndex).toBe(2);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowLeftEvent);
}, false);
expect(context.selectedCommitIndex).toBe(1);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowLeftEvent);
}, false);
expect(context.selectedCommitIndex).toBe(0);

// Cleanup
await utils.actAsync(() => profilerRoot.unmount());
document.body.removeChild(profilerContainer);
});

it('should handle commit selection edge cases when filtering commits', async () => {
const Scheduler = require('scheduler');

// Create components that do varying amounts of work to generate different commit durations
const Parent = ({count}) => {
Scheduler.unstable_advanceTime(10);
const items = [];
for (let i = 0; i < count; i++) {
items.push(<Child key={i} duration={i} />);
}
return <div>{items}</div>;
};
const Child = ({duration}) => {
Scheduler.unstable_advanceTime(duration);
return <span>{duration}</span>;
};

const container = document.createElement('div');
const root = ReactDOMClient.createRoot(container);
utils.act(() => root.render(<Parent count={1} />));

// Profile and record multiple commits with different amounts of work
await utils.actAsync(() => store.profilerStore.startProfiling());
await utils.actAsync(() => root.render(<Parent count={5} />)); // Commit 1 - 20ms
await utils.actAsync(() => root.render(<Parent count={20} />)); // Commit 2 - 200ms
await utils.actAsync(() => root.render(<Parent count={50} />)); // Commit 3 - 1235ms
await utils.actAsync(() => root.render(<Parent count={10} />)); // Commit 4 - 55ms
await utils.actAsync(() => store.profilerStore.stopProfiling());

// Context providers
const Profiler =
require('react-devtools-shared/src/devtools/views/Profiler/Profiler').default;
const {
TimelineContextController,
} = require('react-devtools-timeline/src/TimelineContext');
const {
SettingsContextController,
} = require('react-devtools-shared/src/devtools/views/Settings/SettingsContext');
const {
ModalDialogContextController,
} = require('react-devtools-shared/src/devtools/views/ModalDialog');

let context: Context = ((null: any): Context);
function ContextReader() {
context = React.useContext(ProfilerContext);
return null;
}

const profilerContainer = document.createElement('div');
document.body.appendChild(profilerContainer);

const profilerRoot = ReactDOMClient.createRoot(profilerContainer);

await utils.actAsync(() => {
profilerRoot.render(
<Contexts>
<SettingsContextController browserTheme="light">
<ModalDialogContextController>
<TimelineContextController>
<Profiler />
<ContextReader />
</TimelineContextController>
</ModalDialogContextController>
</SettingsContextController>
</Contexts>,
);
});

// Verify we have profiling data with 4 commits
expect(context.didRecordCommits).toBe(true);
expect(context.profilingData).not.toBeNull();
const rootID = context.rootID;
expect(rootID).not.toBeNull();
const dataForRoot = context.profilingData.dataForRoots.get(rootID);
expect(dataForRoot.commitData.length).toBe(4);
// Edge case 1: Should start at the first commit
expect(context.selectedCommitIndex).toBe(0);

const ownerWindow = profilerContainer.ownerDocument.defaultView;
const isMac =
typeof navigator !== 'undefined' &&
navigator.platform.toUpperCase().indexOf('MAC') >= 0;

const arrowRightEvent = new KeyboardEvent('keydown', {
key: 'ArrowRight',
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
});

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(1);

await utils.actAsync(() => {
context.setIsCommitFilterEnabled(true);
});

// Edge case 2: When filtering is enabled, selected commit should remain if it's still visible
expect(context.filteredCommitIndices.length).toBe(4);
expect(context.selectedCommitIndex).toBe(1);
expect(context.selectedFilteredCommitIndex).toBe(1);

await utils.actAsync(() => {
context.setMinCommitDuration(1000000);
});

// Edge case 3: When all commits are filtered out, selection should be null
expect(context.filteredCommitIndices).toEqual([]);
expect(context.selectedCommitIndex).toBe(null);
expect(context.selectedFilteredCommitIndex).toBe(null);

await utils.actAsync(() => {
context.setMinCommitDuration(0);
});

// Edge case 4: After restoring commits, first commit should be auto-selected
expect(context.filteredCommitIndices.length).toBe(4);
expect(context.selectedCommitIndex).toBe(0);
expect(context.selectedFilteredCommitIndex).toBe(0);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(1);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(2);

await utils.actAsync(() => {
ownerWindow.dispatchEvent(arrowRightEvent);
}, false);
expect(context.selectedCommitIndex).toBe(3);

// Filter out the currently selected commit using actual commit data
const commitDurations = dataForRoot.commitData.map(
commit => commit.duration,
);
const selectedCommitDuration = commitDurations[3];
const filterThreshold = selectedCommitDuration + 0.001;
await utils.actAsync(() => {
context.setMinCommitDuration(filterThreshold);
});

// Edge case 5: Should auto-select first available commit when current one is filtered
expect(context.selectedCommitIndex).not.toBe(null);
expect(context.selectedFilteredCommitIndex).toBe(1);

await utils.actAsync(() => {
context.setIsCommitFilterEnabled(false);
});

// Edge case 6: When filtering is disabled, selected commit should remain
expect(context.filteredCommitIndices.length).toBe(4);
expect(context.selectedCommitIndex).toBe(2);
expect(context.selectedFilteredCommitIndex).toBe(2);

await utils.actAsync(() => profilerRoot.unmount());
document.body.removeChild(profilerContainer);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ function Profiler(_: {}) {
supportsProfiling,
startProfiling,
stopProfiling,
selectPrevCommitIndex,
selectNextCommitIndex,
} = useContext(ProfilerContext);

const {file: timelineTraceEventData, searchInputContainerRef} =
Expand All @@ -63,9 +65,9 @@ function Profiler(_: {}) {

const isLegacyProfilerSelected = selectedTabID !== 'timeline';

// Cmd+E to start/stop profiler recording
const handleKeyDown = useEffectEvent((event: KeyboardEvent) => {
const correctModifier = isMac ? event.metaKey : event.ctrlKey;
// Cmd+E to start/stop profiler recording
if (correctModifier && event.key === 'e') {
if (isProfiling) {
stopProfiling();
Expand All @@ -74,6 +76,24 @@ function Profiler(_: {}) {
}
event.preventDefault();
event.stopPropagation();
} else if (
isLegacyProfilerSelected &&
didRecordCommits &&
selectedCommitIndex !== null
) {
// Cmd+Left/Right (Mac) or Ctrl+Left/Right (Windows/Linux) to navigate commits
if (
correctModifier &&
(event.key === 'ArrowLeft' || event.key === 'ArrowRight')
) {
if (event.key === 'ArrowLeft') {
selectPrevCommitIndex();
} else {
selectNextCommitIndex();
}
event.preventDefault();
event.stopPropagation();
}
}
});

Expand Down
Loading
Loading