From cc7ea75440df571e28addcbf3023cbcbee4cf486 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Sun, 5 Jul 2026 03:36:58 +0500 Subject: [PATCH 01/26] Added hook for timeline data --- src/hooks/useTimelineData.ts | 143 +++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/hooks/useTimelineData.ts diff --git a/src/hooks/useTimelineData.ts b/src/hooks/useTimelineData.ts new file mode 100644 index 00000000..a505b4a7 --- /dev/null +++ b/src/hooks/useTimelineData.ts @@ -0,0 +1,143 @@ +import { + type PickleTag, + type TestCaseStarted, + TestStepResultStatus, + TimeConversion, +} from "@cucumber/messages"; +import { useMemo } from "react"; + +import { useQueries } from "./useQueries.js"; +import { useSearch } from "./useSearch.js"; + +export interface TimelineItem { + readonly id: string; + readonly groupId: string; + readonly groupLabel: string; + readonly feature: string; + readonly scenario: string; + readonly tags: readonly PickleTag[]; + readonly status: TestStepResultStatus; + readonly start: number; + readonly end: number; + readonly testCaseStarted: TestCaseStarted; +} + +export interface TimelineGroup { + readonly id: string; + readonly label: string; +} + +export interface TimelineData { + readonly groups: readonly TimelineGroup[]; + readonly items: readonly TimelineItem[]; + readonly start: number; + readonly end: number; + readonly filtered: boolean; +} + +const UNASSIGNED_GROUP_ID = ""; + +export function useTimelineData(): TimelineData { + const { cucumberQuery } = useQueries(); + const { hideStatuses, tagExpression, searchTerm, unchanged } = useSearch(); + + return useMemo(() => { + const items: TimelineItem[] = []; + const groupIds = new Set(); + const normalizedSearchTerm = searchTerm?.trim().toLowerCase(); + + for (const testCaseFinished of cucumberQuery.findAllTestCaseFinished()) { + const testCaseStarted = + cucumberQuery.findTestCaseStartedBy(testCaseFinished); + if (!testCaseStarted) { + continue; + } + const pickle = cucumberQuery.findPickleBy(testCaseStarted); + if (!pickle) { + continue; + } + + // A test case with no step results at all is considered passed by definition + const status = + cucumberQuery.findMostSevereTestStepResultBy(testCaseFinished) + ?.status ?? TestStepResultStatus.PASSED; + + if (hideStatuses.includes(status)) { + continue; + } + + if (tagExpression) { + const tagNames = pickle.tags.map((tag) => tag.name); + if (!tagExpression.evaluate(tagNames)) { + continue; + } + } + + const feature = + cucumberQuery.findLineageBy(testCaseStarted)?.feature?.name ?? + ""; + const scenario = pickle.name; + + if ( + normalizedSearchTerm && + !`${feature} ${scenario}` + .toLowerCase() + .includes(normalizedSearchTerm) + ) { + continue; + } + + const groupId = testCaseStarted.workerId ?? UNASSIGNED_GROUP_ID; + groupIds.add(groupId); + + items.push({ + id: testCaseStarted.id, + groupId, + groupLabel: describeGroup(groupId), + feature, + scenario, + tags: pickle.tags, + status, + start: TimeConversion.timestampToMillisecondsSinceEpoch( + testCaseStarted.timestamp, + ), + end: TimeConversion.timestampToMillisecondsSinceEpoch( + testCaseFinished.timestamp, + ), + testCaseStarted, + }); + } + + items.sort((a, b) => a.start - b.start || a.end - b.end); + + const groups: TimelineGroup[] = [...groupIds] + .sort(compareGroupIds) + .map((id) => ({ id, label: describeGroup(id) })); + + const start = + items.length > 0 ? Math.min(...items.map((item) => item.start)) : 0; + const end = + items.length > 0 ? Math.max(...items.map((item) => item.end)) : 0; + + return { groups, items, start, end, filtered: !unchanged }; + }, [cucumberQuery, hideStatuses, tagExpression, searchTerm, unchanged]); +} + +function describeGroup(id: string): string { + return id === UNASSIGNED_GROUP_ID ? "Main process" : `Worker ${id}`; +} + +function compareGroupIds(a: string, b: string): number { + if (a === UNASSIGNED_GROUP_ID) { + return -1; + } + if (b === UNASSIGNED_GROUP_ID) { + return 1; + } + const aNum = Number(a); + const bNum = Number(b); + if (!Number.isNaN(aNum) && !Number.isNaN(bNum)) { + return aNum - bNum; + } + return a.localeCompare(b); +} From 37f899c49cc95c1272fbd7a295c4571a31c0934b Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Sun, 5 Jul 2026 03:41:53 +0500 Subject: [PATCH 02/26] Created React component for Timeline --- src/components/app/Timeline.module.scss | 174 ++++++++++++++++++++++++ src/components/app/Timeline.tsx | 136 ++++++++++++++++++ 2 files changed, 310 insertions(+) create mode 100644 src/components/app/Timeline.module.scss create mode 100644 src/components/app/Timeline.tsx diff --git a/src/components/app/Timeline.module.scss b/src/components/app/Timeline.module.scss new file mode 100644 index 00000000..3cb8622b --- /dev/null +++ b/src/components/app/Timeline.module.scss @@ -0,0 +1,174 @@ +@use '../../styles/statuses'; +@use '../../styles/theming'; + +.container { + display: flex; + flex-direction: column; + gap: 1em; +} + +.empty { + font-style: italic; +} + +.chart { + position: relative; + overflow-x: auto; +} + +.axis { + position: relative; + height: 1.5em; + margin: 0 0 0.5em; + padding: 0; + list-style: none; + border-bottom: 1px solid theming.$panelAccentColor; + min-width: 40em; +} + +.tick { + position: absolute; + top: 0; + bottom: 0; + border-left: 1px dashed theming.$panelAccentColor; + padding-left: 0.35em; + font-size: 0.75em; + opacity: 0.75; + white-space: nowrap; + + &[data-edge='end'] { + border-left: none; + border-right: 1px dashed theming.$panelAccentColor; + padding-left: 0; + padding-right: 0.35em; + text-align: right; + + span { + display: inline-block; + transform: translateX(-100%); + } + } +} + +.groups { + display: flex; + flex-direction: column; + gap: 0.25em; + padding: 0; + margin: 0; + list-style: none; + min-width: 40em; +} + +.group { + display: flex; + align-items: center; + gap: 0.5em; +} + +.groupLabel { + flex: 0 0 auto; + width: 8em; + font-size: 0.85em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + opacity: 0.75; +} + +.lane { + position: relative; + flex: 1 1 auto; + height: 2.25em; + background-color: theming.$panelBackgroundColor; + border-radius: 0.25em; +} + +.item { + position: absolute; + top: 0.25em; + bottom: 0.25em; + min-width: 6px; + padding: 0 0.4em; + overflow: hidden; + border: none; + border-radius: 0.2em; + cursor: pointer; + color: white; + font: inherit; + font-size: 0.75em; + text-align: left; + + @each $name, $color in statuses.$statusColors { + &[data-status='#{$name}'] { + background-color: $color; + } + } + + &[aria-pressed='true'] { + outline: 2px solid theming.$panelTextColor; + outline-offset: 1px; + z-index: 1; + } +} + +.itemLabel { + display: block; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.detail { + position: relative; + padding: 1em; + background-color: theming.$panelBackgroundColor; + color: theming.$panelTextColor; + border: 1px solid theming.$panelAccentColor; + border-radius: 0.25em; +} + +.detailClose { + position: absolute; + top: 0.5em; + right: 0.5em; + padding: 0.25em; + background: none; + border: none; + cursor: pointer; + color: inherit; +} + +.detailTitle { + display: flex; + align-items: center; + gap: 0.4em; + margin: 0 0 0.25em; + font-size: 1.1em; + + svg { + height: 1em; + } +} + +.detailFeature { + margin: 0 0 0.5em; + opacity: 0.75; +} + +.detailMeta { + display: flex; + flex-wrap: wrap; + gap: 1em; + margin: 0.75em 0 0; + + dt { + font-size: 0.75em; + text-transform: uppercase; + opacity: 0.75; + } + + dd { + margin: 0; + } +} \ No newline at end of file diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx new file mode 100644 index 00000000..e121fc4b --- /dev/null +++ b/src/components/app/Timeline.tsx @@ -0,0 +1,136 @@ +import { faXmark } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { type FC, useState } from 'react' + +import { formatExecutionDuration } from '../../formatExecutionDuration.js' +import { type TimelineItem, useTimelineData } from '../../hooks/useTimelineData.js' +import { StatusIcon } from '../gherkin/StatusIcon.js' +import statusName from '../gherkin/statusName.js' +import { Tags } from '../gherkin/Tags.js' +import styles from './Timeline.module.scss' + +const AXIS_TICKS = 4 + +export const Timeline: FC = () => { + const { groups, items, start, end, filtered } = useTimelineData() + const [selectedId, setSelectedId] = useState() + + if (items.length === 0) { + return filtered ? ( +

No scenarios match your query and/or filters.

+ ) : ( +

No scenarios were executed.

+ ) + } + + const duration = Math.max(end - start, 1) + const ticks = Array.from({ length: AXIS_TICKS + 1 }, (_, index) => { + const offset = (duration / AXIS_TICKS) * index + return { + index, + position: (offset / duration) * 100, + label: formatExecutionDuration(new Date(start), new Date(start + offset)), + } + }) + const selectedItem = items.find((item) => item.id === selectedId) + + return ( +
+
+ +
    + {groups.map((group) => ( +
  1. + {group.label} +
    + {items + .filter((item) => item.groupId === group.id) + .map((item) => ( + + setSelectedId((current) => (current === item.id ? undefined : item.id)) + } + /> + ))} +
    +
  2. + ))} +
+
+ {selectedItem && ( + setSelectedId(undefined)} /> + )} +
+ ) +} + +const TimelineBar: FC<{ + item: TimelineItem + rangeStart: number + duration: number + selected: boolean + onSelect: () => void +}> = ({ item, rangeStart, duration, selected, onSelect }) => { + const left = ((item.start - rangeStart) / duration) * 100 + const width = Math.max(((item.end - item.start) / duration) * 100, 0.3) + return ( + + ) +} + +const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item, onClose }) => { + return ( +
+ +

+ + {item.scenario} +

+ {item.feature &&

{item.feature}

} + +
+
+
Status
+
{statusName(item.status)}
+
+
+
Duration
+
{formatExecutionDuration(new Date(item.start), new Date(item.end))}
+
+
+
Worker
+
{item.groupLabel}
+
+
+
+ ) +} From a3aa4afc1149c24a45fe1fb1f77b17441a12c215 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Sun, 5 Jul 2026 03:46:38 +0500 Subject: [PATCH 03/26] Added tests for Timeline component --- src/components/app/Report.tsx | 5 + src/components/app/Timeline.spec.tsx | 136 ++++++++++++++++++++++++ src/components/app/Timeline.stories.tsx | 71 +++++++++++++ src/components/app/index.ts | 1 + 4 files changed, 213 insertions(+) create mode 100644 src/components/app/Timeline.spec.tsx create mode 100644 src/components/app/Timeline.stories.tsx diff --git a/src/components/app/Report.tsx b/src/components/app/Report.tsx index f6e2a61c..2eb9f50d 100644 --- a/src/components/app/Report.tsx +++ b/src/components/app/Report.tsx @@ -5,6 +5,7 @@ import { FilteredDocuments } from './FilteredDocuments.js' import styles from './Report.module.scss' import { SearchBar } from './SearchBar.js' import { TestRunHooks } from './TestRunHooks.js' +import { Timeline } from './Timeline.js' export const Report: FC = () => { return ( @@ -13,6 +14,10 @@ export const Report: FC = () => { +
+

Timeline

+ +

Scenarios

diff --git a/src/components/app/Timeline.spec.tsx b/src/components/app/Timeline.spec.tsx new file mode 100644 index 00000000..dfdeeb23 --- /dev/null +++ b/src/components/app/Timeline.spec.tsx @@ -0,0 +1,136 @@ +import { type Envelope, type TestCaseStarted, TestStepResultStatus } from '@cucumber/messages' +import { render, screen, within } from '@testing-library/react' +import { userEvent } from '@testing-library/user-event' +import { expect } from 'chai' + +import examplesTablesFeature from '../../../acceptance/examples-tables/examples-tables.js' +import { ControlledSearchProvider } from './ControlledSearchProvider.js' +import { EnvelopesProvider } from './EnvelopesProvider.js' +import { Timeline } from './Timeline.js' + +describe('', () => { + it('should show a message when no scenarios were executed', () => { + render( + + {}}> + + + + ) + + expect(screen.getByText('No scenarios were executed.')).to.be.visible + }) + + it('should show a message when filters exclude every scenario', () => { + render( + + {}} + > + + + + ) + + expect(screen.getByText('No scenarios match your query and/or filters.')).to.be.visible + }) + + it('should render one bar per executed scenario, in a single lane when no worker information is present', () => { + render( + + {}}> + + + + ) + + expect(screen.getByText('Main process')).to.be.visible + expect(screen.getAllByRole('button')).to.have.length(7) + }) + + it('should respect the hideStatuses filter from the shared search context', () => { + render( + + {}} + > + + + + ) + + expect(screen.getAllByRole('button')).to.have.length(5) + }) + + it('should respect a tag expression from the shared search context', () => { + render( + + {}} + > + + + + ) + + expect(screen.getAllByRole('button')).to.have.length(2) + }) + + it('should group test cases by worker id, sorted numerically', () => { + render( + + {}}> + + + + ) + + expect(screen.getAllByTestId('cucumber.timeline.group')).to.have.length(2) + expect(screen.getByText('Worker 0')).to.be.visible + expect(screen.getByText('Worker 1')).to.be.visible + }) + + it('should show scenario details when a bar is selected, and hide them again on close', async () => { + render( + + {}}> + + + + ) + + expect(screen.queryByTestId('cucumber.timeline.detail')).to.be.null + + await userEvent.click(screen.getByRole('button', { name: 'Eating cucumbers with 11 friends' })) + + const detail = screen.getByTestId('cucumber.timeline.detail') + expect(within(detail).getByText('Eating cucumbers with 11 friends')).to.be.visible + expect(within(detail).getByText('Examples Tables')).to.be.visible + + await userEvent.click(within(detail).getByRole('button', { name: 'Close' })) + + expect(screen.queryByTestId('cucumber.timeline.detail')).to.be.null + }) +}) + +function distributeAcrossWorkers( + envelopes: ReadonlyArray, + workerCount: number +): ReadonlyArray { + let index = 0 + return envelopes.map((envelope): Envelope => { + if (!envelope.testCaseStarted) { + return envelope + } + const workerId = String(index % workerCount) + index += 1 + const testCaseStarted: TestCaseStarted = { ...envelope.testCaseStarted, workerId } + return { ...envelope, testCaseStarted } + }) +} diff --git a/src/components/app/Timeline.stories.tsx b/src/components/app/Timeline.stories.tsx new file mode 100644 index 00000000..ff21998c --- /dev/null +++ b/src/components/app/Timeline.stories.tsx @@ -0,0 +1,71 @@ +import { type Envelope, type TestCaseStarted, TimeConversion } from '@cucumber/messages' +import type { Story } from '@ladle/react' + +import examplesTablesFeature from '../../../acceptance/examples-tables/examples-tables.js' +import { EnvelopesProvider } from './EnvelopesProvider.js' +import { InMemorySearchProvider } from './InMemorySearchProvider.js' +import { Timeline } from './Timeline.js' + +export default { + title: 'App/Timeline', +} + +type TemplateArgs = { + envelopes: readonly Envelope[] +} + +const Template: Story = ({ envelopes }) => { + return ( + + + + + + ) +} + +export const SingleProcess = Template.bind({}) +SingleProcess.args = { + envelopes: examplesTablesFeature, +} as TemplateArgs + +export const Parallel = Template.bind({}) +Parallel.args = { + envelopes: distributeAcrossWorkers(examplesTablesFeature, 3), +} as TemplateArgs + +export const NoTestCases = Template.bind({}) +NoTestCases.args = { + envelopes: [ + { testRunStarted: { timestamp: TimeConversion.millisecondsSinceEpochToTimestamp(0) } }, + { + testRunFinished: { + timestamp: TimeConversion.millisecondsSinceEpochToTimestamp(1000), + success: true, + }, + }, + ], +} as TemplateArgs + +/** + * Cucumber implementations report which worker ran a test case via + * `TestCaseStarted.workerId`. The compatibility-kit fixtures used in this story + * were captured from a single-process run so this helper distributes the + * existing test cases across a number of synthetic workers to demonstrate how + * the timeline renders parallel execution. + */ +function distributeAcrossWorkers( + envelopes: ReadonlyArray, + workerCount: number +): ReadonlyArray { + let index = 0 + return envelopes.map((envelope): Envelope => { + if (!envelope.testCaseStarted) { + return envelope + } + const workerId = String(index % workerCount) + index += 1 + const testCaseStarted: TestCaseStarted = { ...envelope.testCaseStarted, workerId } + return { ...envelope, testCaseStarted } + }) +} \ No newline at end of file diff --git a/src/components/app/index.ts b/src/components/app/index.ts index 890c590d..6c3b0676 100644 --- a/src/components/app/index.ts +++ b/src/components/app/index.ts @@ -12,3 +12,4 @@ export * from './SearchBar.js' export * from './StatusesSummary.js' export * from './TestRunHooks.js' export * from './UrlSearchProvider.js' +export * from './Timeline.js' From c17bb022562eaddf0dfb4c2f5866dc718ecc54cf Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Sun, 5 Jul 2026 03:51:39 +0500 Subject: [PATCH 04/26] Updated changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 80bd6c78..84d10cf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -583,3 +583,7 @@ to rebuild them every time the envelope list is updated. Use this instead of `` component showing scenario execution over time grouped by worker ported from cucumber-jvm's TimelineFormatter ([#126](https://github.com/cucumber/react-components/issues/126)) From b96cd7edd3dbc946331d1e0a1a40e2a8e2d2c2df Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 7 Jul 2026 01:44:25 +0500 Subject: [PATCH 05/26] Added Tabs in Report to switch between two formats --- src/components/app/Report.module.scss | 36 +++++++++++++++++++++++++++ src/components/app/Report.tsx | 27 ++++++++++++++------ 2 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/components/app/Report.module.scss b/src/components/app/Report.module.scss index b8d3c8f4..dfbff60e 100644 --- a/src/components/app/Report.module.scss +++ b/src/components/app/Report.module.scss @@ -1,3 +1,5 @@ +@use '../../styles/theming'; + .layout { > section:not(:last-child) { margin-bottom: 1.5em; @@ -8,3 +10,37 @@ font-size: 1.25em; margin: 1em 0; } + +.tabList { + display: flex; + gap: 0.25em; + border-bottom: 1px solid theming.$panelAccentColor; + margin-bottom: 1em; +} + +.tab { + padding: 0.5em 1em; + border-bottom: 2px solid transparent; + color: theming.$panelTextColor; + cursor: pointer; + outline: none; + + &[data-hovered] { + background-color: theming.$panelBackgroundColor; + } + + &[data-selected] { + border-bottom-color: theming.$anchorColor; + color: theming.$anchorColor; + font-weight: 600; + } + + &[data-focus-visible] { + outline: 2px solid theming.$anchorColor; + outline-offset: 2px; + } +} + +.tabPanel { + outline: none; +} \ No newline at end of file diff --git a/src/components/app/Report.tsx b/src/components/app/Report.tsx index 2eb9f50d..0bcabc46 100644 --- a/src/components/app/Report.tsx +++ b/src/components/app/Report.tsx @@ -1,4 +1,5 @@ import type { FC } from 'react' +import { Tab, TabList, TabPanel, TabPanels, Tabs } from 'react-aria-components' import { ExecutionSummary } from './ExecutionSummary.js' import { FilteredDocuments } from './FilteredDocuments.js' @@ -15,12 +16,24 @@ export const Report: FC = () => {
-

Timeline

- -
-
-

Scenarios

- + + + + Scenarios + + + Timeline + + + + + + + + + + +

Hooks

@@ -28,4 +41,4 @@ export const Report: FC = () => {
) -} +} \ No newline at end of file From ad1238db20f14e10dc2ba179fd222fb04abd7127 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 7 Jul 2026 13:59:38 +0500 Subject: [PATCH 06/26] style: fix lint formatting issues --- src/components/app/Report.tsx | 2 +- src/components/app/Timeline.stories.tsx | 2 +- src/components/app/index.ts | 2 +- src/hooks/useTimelineData.ts | 223 +++++++++++------------- 4 files changed, 109 insertions(+), 120 deletions(-) diff --git a/src/components/app/Report.tsx b/src/components/app/Report.tsx index 0bcabc46..5fa206b9 100644 --- a/src/components/app/Report.tsx +++ b/src/components/app/Report.tsx @@ -41,4 +41,4 @@ export const Report: FC = () => { ) -} \ No newline at end of file +} diff --git a/src/components/app/Timeline.stories.tsx b/src/components/app/Timeline.stories.tsx index ff21998c..af99a13f 100644 --- a/src/components/app/Timeline.stories.tsx +++ b/src/components/app/Timeline.stories.tsx @@ -68,4 +68,4 @@ function distributeAcrossWorkers( const testCaseStarted: TestCaseStarted = { ...envelope.testCaseStarted, workerId } return { ...envelope, testCaseStarted } }) -} \ No newline at end of file +} diff --git a/src/components/app/index.ts b/src/components/app/index.ts index 6c3b0676..b6399b7b 100644 --- a/src/components/app/index.ts +++ b/src/components/app/index.ts @@ -11,5 +11,5 @@ export * from './Report.js' export * from './SearchBar.js' export * from './StatusesSummary.js' export * from './TestRunHooks.js' -export * from './UrlSearchProvider.js' export * from './Timeline.js' +export * from './UrlSearchProvider.js' diff --git a/src/hooks/useTimelineData.ts b/src/hooks/useTimelineData.ts index a505b4a7..0385a03d 100644 --- a/src/hooks/useTimelineData.ts +++ b/src/hooks/useTimelineData.ts @@ -1,143 +1,132 @@ import { - type PickleTag, - type TestCaseStarted, - TestStepResultStatus, - TimeConversion, -} from "@cucumber/messages"; -import { useMemo } from "react"; + type PickleTag, + type TestCaseStarted, + TestStepResultStatus, + TimeConversion, +} from '@cucumber/messages' +import { useMemo } from 'react' -import { useQueries } from "./useQueries.js"; -import { useSearch } from "./useSearch.js"; +import { useQueries } from './useQueries.js' +import { useSearch } from './useSearch.js' export interface TimelineItem { - readonly id: string; - readonly groupId: string; - readonly groupLabel: string; - readonly feature: string; - readonly scenario: string; - readonly tags: readonly PickleTag[]; - readonly status: TestStepResultStatus; - readonly start: number; - readonly end: number; - readonly testCaseStarted: TestCaseStarted; + readonly id: string + readonly groupId: string + readonly groupLabel: string + readonly feature: string + readonly scenario: string + readonly tags: readonly PickleTag[] + readonly status: TestStepResultStatus + readonly start: number + readonly end: number + readonly testCaseStarted: TestCaseStarted } export interface TimelineGroup { - readonly id: string; - readonly label: string; + readonly id: string + readonly label: string } export interface TimelineData { - readonly groups: readonly TimelineGroup[]; - readonly items: readonly TimelineItem[]; - readonly start: number; - readonly end: number; - readonly filtered: boolean; + readonly groups: readonly TimelineGroup[] + readonly items: readonly TimelineItem[] + readonly start: number + readonly end: number + readonly filtered: boolean } -const UNASSIGNED_GROUP_ID = ""; +const UNASSIGNED_GROUP_ID = '' export function useTimelineData(): TimelineData { - const { cucumberQuery } = useQueries(); - const { hideStatuses, tagExpression, searchTerm, unchanged } = useSearch(); - - return useMemo(() => { - const items: TimelineItem[] = []; - const groupIds = new Set(); - const normalizedSearchTerm = searchTerm?.trim().toLowerCase(); - - for (const testCaseFinished of cucumberQuery.findAllTestCaseFinished()) { - const testCaseStarted = - cucumberQuery.findTestCaseStartedBy(testCaseFinished); - if (!testCaseStarted) { - continue; - } - const pickle = cucumberQuery.findPickleBy(testCaseStarted); - if (!pickle) { - continue; - } - - // A test case with no step results at all is considered passed by definition - const status = - cucumberQuery.findMostSevereTestStepResultBy(testCaseFinished) - ?.status ?? TestStepResultStatus.PASSED; - - if (hideStatuses.includes(status)) { - continue; - } - - if (tagExpression) { - const tagNames = pickle.tags.map((tag) => tag.name); - if (!tagExpression.evaluate(tagNames)) { - continue; - } - } - - const feature = - cucumberQuery.findLineageBy(testCaseStarted)?.feature?.name ?? - ""; - const scenario = pickle.name; - - if ( - normalizedSearchTerm && - !`${feature} ${scenario}` - .toLowerCase() - .includes(normalizedSearchTerm) - ) { - continue; - } - - const groupId = testCaseStarted.workerId ?? UNASSIGNED_GROUP_ID; - groupIds.add(groupId); - - items.push({ - id: testCaseStarted.id, - groupId, - groupLabel: describeGroup(groupId), - feature, - scenario, - tags: pickle.tags, - status, - start: TimeConversion.timestampToMillisecondsSinceEpoch( - testCaseStarted.timestamp, - ), - end: TimeConversion.timestampToMillisecondsSinceEpoch( - testCaseFinished.timestamp, - ), - testCaseStarted, - }); + const { cucumberQuery } = useQueries() + const { hideStatuses, tagExpression, searchTerm, unchanged } = useSearch() + + return useMemo(() => { + const items: TimelineItem[] = [] + const groupIds = new Set() + const normalizedSearchTerm = searchTerm?.trim().toLowerCase() + + for (const testCaseFinished of cucumberQuery.findAllTestCaseFinished()) { + const testCaseStarted = cucumberQuery.findTestCaseStartedBy(testCaseFinished) + if (!testCaseStarted) { + continue + } + const pickle = cucumberQuery.findPickleBy(testCaseStarted) + if (!pickle) { + continue + } + + // A test case with no step results at all is considered passed by definition + const status = + cucumberQuery.findMostSevereTestStepResultBy(testCaseFinished)?.status ?? + TestStepResultStatus.PASSED + + if (hideStatuses.includes(status)) { + continue + } + + if (tagExpression) { + const tagNames = pickle.tags.map((tag) => tag.name) + if (!tagExpression.evaluate(tagNames)) { + continue } + } + + const feature = cucumberQuery.findLineageBy(testCaseStarted)?.feature?.name ?? '' + const scenario = pickle.name + + if ( + normalizedSearchTerm && + !`${feature} ${scenario}`.toLowerCase().includes(normalizedSearchTerm) + ) { + continue + } + + const groupId = testCaseStarted.workerId ?? UNASSIGNED_GROUP_ID + groupIds.add(groupId) + + items.push({ + id: testCaseStarted.id, + groupId, + groupLabel: describeGroup(groupId), + feature, + scenario, + tags: pickle.tags, + status, + start: TimeConversion.timestampToMillisecondsSinceEpoch(testCaseStarted.timestamp), + end: TimeConversion.timestampToMillisecondsSinceEpoch(testCaseFinished.timestamp), + testCaseStarted, + }) + } - items.sort((a, b) => a.start - b.start || a.end - b.end); + items.sort((a, b) => a.start - b.start || a.end - b.end) - const groups: TimelineGroup[] = [...groupIds] - .sort(compareGroupIds) - .map((id) => ({ id, label: describeGroup(id) })); + const groups: TimelineGroup[] = [...groupIds] + .sort(compareGroupIds) + .map((id) => ({ id, label: describeGroup(id) })) - const start = - items.length > 0 ? Math.min(...items.map((item) => item.start)) : 0; - const end = - items.length > 0 ? Math.max(...items.map((item) => item.end)) : 0; + const start = items.length > 0 ? Math.min(...items.map((item) => item.start)) : 0 + const end = items.length > 0 ? Math.max(...items.map((item) => item.end)) : 0 - return { groups, items, start, end, filtered: !unchanged }; - }, [cucumberQuery, hideStatuses, tagExpression, searchTerm, unchanged]); + return { groups, items, start, end, filtered: !unchanged } + }, [cucumberQuery, hideStatuses, tagExpression, searchTerm, unchanged]) } function describeGroup(id: string): string { - return id === UNASSIGNED_GROUP_ID ? "Main process" : `Worker ${id}`; + return id === UNASSIGNED_GROUP_ID ? 'Main process' : `Worker ${id}` } function compareGroupIds(a: string, b: string): number { - if (a === UNASSIGNED_GROUP_ID) { - return -1; - } - if (b === UNASSIGNED_GROUP_ID) { - return 1; - } - const aNum = Number(a); - const bNum = Number(b); - if (!Number.isNaN(aNum) && !Number.isNaN(bNum)) { - return aNum - bNum; - } - return a.localeCompare(b); + if (a === UNASSIGNED_GROUP_ID) { + return -1 + } + if (b === UNASSIGNED_GROUP_ID) { + return 1 + } + const aNum = Number(a) + const bNum = Number(b) + if (!Number.isNaN(aNum) && !Number.isNaN(bNum)) { + return aNum - bNum + } + return a.localeCompare(b) } From e7324685c1e39d40eae670ce7a31f5c62ab60ae0 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 7 Jul 2026 18:47:46 +0500 Subject: [PATCH 07/26] Added sample parallel run envelope --- samples/parallel-run.ts | 63 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 samples/parallel-run.ts diff --git a/samples/parallel-run.ts b/samples/parallel-run.ts new file mode 100644 index 00000000..7032c134 --- /dev/null +++ b/samples/parallel-run.ts @@ -0,0 +1,63 @@ +import type { Envelope } from '@cucumber/messages' + +export default [ + {"meta":{"protocolVersion":"29.0.1","implementation":{"name":"cucumber-jvm","version":"7.30.0"},"runtime":{"name":"Java HotSpot(TM) 64-Bit Server VM","version":"26.0.1+8-34"},"os":{"name":"Linux"},"cpu":{"name":"amd64"}}} +,{"testRunStarted":{"timestamp":{"seconds":1783364910,"nanos":419492005}}} +,{"source":{"uri":"classpath:parallel/scenarios.feature","data":"Feature: Parallel Scenarios\n\n Scenario: One\n Given I wait for 5 seconds\n\n Scenario: Two\n Given I wait for 10 seconds\n\n Scenario: Three\n Given I wait for 15 seconds\n\n Scenario: Four\n Given I wait for 20 seconds","mediaType":"text/x.cucumber.gherkin+plain"}} +,{"gherkinDocument":{"uri":"classpath:parallel/scenarios.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en","keyword":"Feature","name":"Parallel Scenarios","description":"","children":[{"scenario":{"location":{"line":3,"column":3},"tags":[],"keyword":"Scenario","name":"One","description":"","steps":[{"location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"I wait for 5 seconds","id":"ec7dd26c-eff5-4987-9e49-e4d6ebc47367"}],"examples":[],"id":"5ecdab5f-6f2e-4dbc-99bd-5d34087df0db"}},{"scenario":{"location":{"line":6,"column":3},"tags":[],"keyword":"Scenario","name":"Two","description":"","steps":[{"location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"I wait for 10 seconds","id":"aad30de0-0f9d-4aee-aab6-4c9b89d2dc82"}],"examples":[],"id":"9ec6892a-20d6-4179-bc9b-0a5ee5352fcf"}},{"scenario":{"location":{"line":9,"column":3},"tags":[],"keyword":"Scenario","name":"Three","description":"","steps":[{"location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"I wait for 15 seconds","id":"27fb1e9b-2da9-44df-9a40-3b9cd5f255dd"}],"examples":[],"id":"73fddbfe-bed6-4b96-b134-2e40bcbcbe77"}},{"scenario":{"location":{"line":12,"column":3},"tags":[],"keyword":"Scenario","name":"Four","description":"","steps":[{"location":{"line":13,"column":5},"keyword":"Given ","keywordType":"Context","text":"I wait for 20 seconds","id":"23b79b90-7ae6-4614-8790-c5b01ed66d9e"}],"examples":[],"id":"d2e50951-bc48-4c28-b513-0e395ac0a146"}}]},"comments":[]}} +,{"pickle":{"id":"8f92f4e6-d3a5-4db6-bfc7-ef4cd7ab7104","uri":"classpath:parallel/scenarios.feature","name":"One","language":"en","steps":[{"astNodeIds":["ec7dd26c-eff5-4987-9e49-e4d6ebc47367"],"id":"5f9baeb1-a1c0-4e58-9dbc-83e64836c83c","type":"Context","text":"I wait for 5 seconds"}],"tags":[],"astNodeIds":["5ecdab5f-6f2e-4dbc-99bd-5d34087df0db"]}} +,{"pickle":{"id":"ab4838f7-ff58-4ac6-885a-f9532b218146","uri":"classpath:parallel/scenarios.feature","name":"Two","language":"en","steps":[{"astNodeIds":["aad30de0-0f9d-4aee-aab6-4c9b89d2dc82"],"id":"ae6e653e-d568-4c33-a99d-0653393bf03a","type":"Context","text":"I wait for 10 seconds"}],"tags":[],"astNodeIds":["9ec6892a-20d6-4179-bc9b-0a5ee5352fcf"]}} +,{"pickle":{"id":"742927e4-043d-404c-aea3-83edf921a0aa","uri":"classpath:parallel/scenarios.feature","name":"Three","language":"en","steps":[{"astNodeIds":["27fb1e9b-2da9-44df-9a40-3b9cd5f255dd"],"id":"a96d5267-582e-4857-9d96-6816dc3e4735","type":"Context","text":"I wait for 15 seconds"}],"tags":[],"astNodeIds":["73fddbfe-bed6-4b96-b134-2e40bcbcbe77"]}} +,{"pickle":{"id":"90869e14-d1a1-46f8-b959-5e8d937772dd","uri":"classpath:parallel/scenarios.feature","name":"Four","language":"en","steps":[{"astNodeIds":["23b79b90-7ae6-4614-8790-c5b01ed66d9e"],"id":"9ed17bf5-1c87-4dba-b3a4-0f7195efa9e7","type":"Context","text":"I wait for 20 seconds"}],"tags":[],"astNodeIds":["d2e50951-bc48-4c28-b513-0e395ac0a146"]}} +,{"source":{"uri":"classpath:parallel/scenario-outlines.feature","data":"Feature: Scenario Outline\n\n Scenario Outline: Waiting\n\n Given I wait for seconds\n\n Examples:\n | seconds |\n | 5 |\n | 10 |\n | 15 |\n | 20 |","mediaType":"text/x.cucumber.gherkin+plain"}} +,{"gherkinDocument":{"uri":"classpath:parallel/scenario-outlines.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en","keyword":"Feature","name":"Scenario Outline","description":"","children":[{"scenario":{"location":{"line":3,"column":3},"tags":[],"keyword":"Scenario Outline","name":"Waiting","description":"","steps":[{"location":{"line":5,"column":5},"keyword":"Given ","keywordType":"Context","text":"I wait for seconds","id":"9939e61a-a7ad-4164-b84a-73d97e0e1c0b"}],"examples":[{"location":{"line":7,"column":5},"tags":[],"keyword":"Examples","name":"","description":"","tableHeader":{"location":{"line":8,"column":7},"cells":[{"location":{"line":8,"column":9},"value":"seconds"}],"id":"4b96e4fe-035a-4781-9663-75312d646d6b"},"tableBody":[{"location":{"line":9,"column":7},"cells":[{"location":{"line":9,"column":9},"value":"5"}],"id":"4946302f-b4cf-4e73-9eed-634a9a099c55"},{"location":{"line":10,"column":7},"cells":[{"location":{"line":10,"column":9},"value":"10"}],"id":"a640ecbd-23b0-469d-bf24-4b72e45d5812"},{"location":{"line":11,"column":7},"cells":[{"location":{"line":11,"column":9},"value":"15"}],"id":"05a33b74-9aa0-412c-bcee-abcc26343512"},{"location":{"line":12,"column":7},"cells":[{"location":{"line":12,"column":9},"value":"20"}],"id":"3fbf7a14-5518-4518-bb90-0ac06dcf6aff"}],"id":"f250783f-e983-484d-af73-981bb08fed50"}],"id":"95e218da-cab5-4fbf-beea-6558b4747cdb"}}]},"comments":[]}} +,{"pickle":{"id":"5d6d82e8-a595-4263-844e-6f16accdc8df","uri":"classpath:parallel/scenario-outlines.feature","name":"Waiting","language":"en","steps":[{"astNodeIds":["9939e61a-a7ad-4164-b84a-73d97e0e1c0b","4946302f-b4cf-4e73-9eed-634a9a099c55"],"id":"fb4414fa-e52d-4fbe-9b8c-eff368dc72b5","type":"Context","text":"I wait for 5 seconds"}],"tags":[],"astNodeIds":["95e218da-cab5-4fbf-beea-6558b4747cdb","4946302f-b4cf-4e73-9eed-634a9a099c55"]}} +,{"pickle":{"id":"3986b982-1a48-4774-9ada-8a9e0349aa02","uri":"classpath:parallel/scenario-outlines.feature","name":"Waiting","language":"en","steps":[{"astNodeIds":["9939e61a-a7ad-4164-b84a-73d97e0e1c0b","a640ecbd-23b0-469d-bf24-4b72e45d5812"],"id":"b0fecc18-4e3f-432f-8182-6449940ce593","type":"Context","text":"I wait for 10 seconds"}],"tags":[],"astNodeIds":["95e218da-cab5-4fbf-beea-6558b4747cdb","a640ecbd-23b0-469d-bf24-4b72e45d5812"]}} +,{"pickle":{"id":"62d538a3-805a-4e77-aa84-2c4cdd23b78a","uri":"classpath:parallel/scenario-outlines.feature","name":"Waiting","language":"en","steps":[{"astNodeIds":["9939e61a-a7ad-4164-b84a-73d97e0e1c0b","05a33b74-9aa0-412c-bcee-abcc26343512"],"id":"9961450c-9f4d-4fda-b0bf-e4990beade63","type":"Context","text":"I wait for 15 seconds"}],"tags":[],"astNodeIds":["95e218da-cab5-4fbf-beea-6558b4747cdb","05a33b74-9aa0-412c-bcee-abcc26343512"]}} +,{"pickle":{"id":"8c7958a6-f6cb-4798-8f0f-7db60a263f1e","uri":"classpath:parallel/scenario-outlines.feature","name":"Waiting","language":"en","steps":[{"astNodeIds":["9939e61a-a7ad-4164-b84a-73d97e0e1c0b","3fbf7a14-5518-4518-bb90-0ac06dcf6aff"],"id":"1f45b7e6-f3ab-435c-80bd-a8dee0219b76","type":"Context","text":"I wait for 20 seconds"}],"tags":[],"astNodeIds":["95e218da-cab5-4fbf-beea-6558b4747cdb","3fbf7a14-5518-4518-bb90-0ac06dcf6aff"]}} +,{"stepDefinition":{"id":"122528c2-c6e9-49a1-ad3f-9cb365feaad0","pattern":{"source":"I wait for {int} seconds","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"parallel.StepDefinitions","methodName":"waitFor","methodParameterTypes":["int"]}}}} +,{"stepDefinition":{"id":"9d239edf-87f2-4821-bfa0-bdff16aba193","pattern":{"source":"I wait for {int} seconds","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"parallel.StepDefinitions","methodName":"waitFor","methodParameterTypes":["int"]}}}} +,{"stepDefinition":{"id":"c582c113-0461-4f5c-a1bd-db373063bfd4","pattern":{"source":"I wait for {int} seconds","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"parallel.StepDefinitions","methodName":"waitFor","methodParameterTypes":["int"]}}}} +,{"stepDefinition":{"id":"0b938f0d-a79d-4d4a-90c5-51e72abb8160","pattern":{"source":"I wait for {int} seconds","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"parallel.StepDefinitions","methodName":"waitFor","methodParameterTypes":["int"]}}}} +,{"testCase":{"id":"e191b6b7-a04b-4d30-a23b-58b05df68c89","pickleId":"ab4838f7-ff58-4ac6-885a-f9532b218146","testSteps":[{"id":"e164c9fb-06e3-49a5-8725-a9a8b2e9c28c","pickleStepId":"ae6e653e-d568-4c33-a99d-0653393bf03a","stepDefinitionIds":["122528c2-c6e9-49a1-ad3f-9cb365feaad0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"10"},"parameterTypeName":"int"}]}]}]}} +,{"testCase":{"id":"7c14d6c9-e1b0-4f90-8264-6c671b1f69c4","pickleId":"8c7958a6-f6cb-4798-8f0f-7db60a263f1e","testSteps":[{"id":"c301dac5-6567-41df-abe6-e78ccbb0d494","pickleStepId":"1f45b7e6-f3ab-435c-80bd-a8dee0219b76","stepDefinitionIds":["c582c113-0461-4f5c-a1bd-db373063bfd4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"20"},"parameterTypeName":"int"}]}]}]}} +,{"testCaseStarted":{"attempt":0,"id":"f247c885-138a-414e-bcb4-f5a2e3dc053d","testCaseId":"7c14d6c9-e1b0-4f90-8264-6c671b1f69c4","workerId":"ForkJoinPool-2-worker-2","timestamp":{"seconds":1783364910,"nanos":655349506}}} +,{"testStepStarted":{"testCaseStartedId":"f247c885-138a-414e-bcb4-f5a2e3dc053d","testStepId":"c301dac5-6567-41df-abe6-e78ccbb0d494","timestamp":{"seconds":1783364910,"nanos":667969143}}} +,{"testCase":{"id":"6f9693a3-16a1-46ee-ba0e-62875dad40fe","pickleId":"90869e14-d1a1-46f8-b959-5e8d937772dd","testSteps":[{"id":"84506d2f-0582-4982-aad1-40a4d08b8987","pickleStepId":"9ed17bf5-1c87-4dba-b3a4-0f7195efa9e7","stepDefinitionIds":["9d239edf-87f2-4821-bfa0-bdff16aba193"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"20"},"parameterTypeName":"int"}]}]}]}} +,{"testCaseStarted":{"attempt":0,"id":"6f571cd2-3514-41ad-aa35-1afdeb863439","testCaseId":"6f9693a3-16a1-46ee-ba0e-62875dad40fe","workerId":"ForkJoinPool-2-worker-1","timestamp":{"seconds":1783364910,"nanos":674377251}}} +,{"testStepStarted":{"testCaseStartedId":"6f571cd2-3514-41ad-aa35-1afdeb863439","testStepId":"84506d2f-0582-4982-aad1-40a4d08b8987","timestamp":{"seconds":1783364910,"nanos":676105417}}} +,{"testCase":{"id":"e5b2bec6-6f10-4e17-954c-5686cf8f8d63","pickleId":"8f92f4e6-d3a5-4db6-bfc7-ef4cd7ab7104","testSteps":[{"id":"d7dfbbeb-a408-414a-813f-f026b4a7fdd0","pickleStepId":"5f9baeb1-a1c0-4e58-9dbc-83e64836c83c","stepDefinitionIds":["0b938f0d-a79d-4d4a-90c5-51e72abb8160"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"5"},"parameterTypeName":"int"}]}]}]}} +,{"testCaseStarted":{"attempt":0,"id":"0d58f229-68a7-471d-bdc2-4333844acf0b","testCaseId":"e5b2bec6-6f10-4e17-954c-5686cf8f8d63","workerId":"ForkJoinPool-2-worker-3","timestamp":{"seconds":1783364910,"nanos":677951850}}} +,{"testCaseStarted":{"attempt":0,"id":"f623c1cb-89a0-4a50-b279-341512fd4d09","testCaseId":"e191b6b7-a04b-4d30-a23b-58b05df68c89","workerId":"ForkJoinPool-2-worker-4","timestamp":{"seconds":1783364910,"nanos":653898230}}} +,{"testStepStarted":{"testCaseStartedId":"0d58f229-68a7-471d-bdc2-4333844acf0b","testStepId":"d7dfbbeb-a408-414a-813f-f026b4a7fdd0","timestamp":{"seconds":1783364910,"nanos":679224081}}} +,{"testStepStarted":{"testCaseStartedId":"f623c1cb-89a0-4a50-b279-341512fd4d09","testStepId":"e164c9fb-06e3-49a5-8725-a9a8b2e9c28c","timestamp":{"seconds":1783364910,"nanos":680352541}}} +,{"testStepFinished":{"testCaseStartedId":"0d58f229-68a7-471d-bdc2-4333844acf0b","testStepId":"d7dfbbeb-a408-414a-813f-f026b4a7fdd0","testStepResult":{"duration":{"seconds":5,"nanos":7209271},"status":"PASSED"},"timestamp":{"seconds":1783364915,"nanos":686433352}}} +,{"testCaseFinished":{"testCaseStartedId":"0d58f229-68a7-471d-bdc2-4333844acf0b","timestamp":{"seconds":1783364915,"nanos":694477859},"willBeRetried":false}} +,{"testCase":{"id":"714fa141-779b-44b5-9e1a-ac7eca676d2e","pickleId":"742927e4-043d-404c-aea3-83edf921a0aa","testSteps":[{"id":"6ee6ca02-77de-4d58-9a14-f5d1976c9409","pickleStepId":"a96d5267-582e-4857-9d96-6816dc3e4735","stepDefinitionIds":["0b938f0d-a79d-4d4a-90c5-51e72abb8160"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"15"},"parameterTypeName":"int"}]}]}]}} +,{"testCaseStarted":{"attempt":0,"id":"58a3ca27-7b8a-4625-9441-67c937a02c44","testCaseId":"714fa141-779b-44b5-9e1a-ac7eca676d2e","workerId":"ForkJoinPool-2-worker-3","timestamp":{"seconds":1783364915,"nanos":704863171}}} +,{"testStepStarted":{"testCaseStartedId":"58a3ca27-7b8a-4625-9441-67c937a02c44","testStepId":"6ee6ca02-77de-4d58-9a14-f5d1976c9409","timestamp":{"seconds":1783364915,"nanos":705906636}}} +,{"testStepFinished":{"testCaseStartedId":"f623c1cb-89a0-4a50-b279-341512fd4d09","testStepId":"e164c9fb-06e3-49a5-8725-a9a8b2e9c28c","testStepResult":{"duration":{"seconds":10,"nanos":4022840},"status":"PASSED"},"timestamp":{"seconds":1783364920,"nanos":684375381}}} +,{"testCaseFinished":{"testCaseStartedId":"f623c1cb-89a0-4a50-b279-341512fd4d09","timestamp":{"seconds":1783364920,"nanos":685612826},"willBeRetried":false}} +,{"testCase":{"id":"709a4ecb-3b44-42de-a2dd-438c643f8cbf","pickleId":"5d6d82e8-a595-4263-844e-6f16accdc8df","testSteps":[{"id":"c023eb48-deee-4058-88de-7caf5ed11a1b","pickleStepId":"fb4414fa-e52d-4fbe-9b8c-eff368dc72b5","stepDefinitionIds":["122528c2-c6e9-49a1-ad3f-9cb365feaad0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"5"},"parameterTypeName":"int"}]}]}]}} +,{"testCaseStarted":{"attempt":0,"id":"6f6868b3-d003-4d37-9485-b559f70cdfd5","testCaseId":"709a4ecb-3b44-42de-a2dd-438c643f8cbf","workerId":"ForkJoinPool-2-worker-4","timestamp":{"seconds":1783364920,"nanos":691156055}}} +,{"testStepStarted":{"testCaseStartedId":"6f6868b3-d003-4d37-9485-b559f70cdfd5","testStepId":"c023eb48-deee-4058-88de-7caf5ed11a1b","timestamp":{"seconds":1783364920,"nanos":692589594}}} +,{"testStepFinished":{"testCaseStartedId":"6f6868b3-d003-4d37-9485-b559f70cdfd5","testStepId":"c023eb48-deee-4058-88de-7caf5ed11a1b","testStepResult":{"duration":{"seconds":5,"nanos":1367400},"status":"PASSED"},"timestamp":{"seconds":1783364925,"nanos":693956994}}} +,{"testCaseFinished":{"testCaseStartedId":"6f6868b3-d003-4d37-9485-b559f70cdfd5","timestamp":{"seconds":1783364925,"nanos":695212572},"willBeRetried":false}} +,{"testCase":{"id":"53456c72-c9bc-48f6-9592-b8d78e5f14d3","pickleId":"3986b982-1a48-4774-9ada-8a9e0349aa02","testSteps":[{"id":"47d5e09f-7426-4e14-b7ee-ee78f3849810","pickleStepId":"b0fecc18-4e3f-432f-8182-6449940ce593","stepDefinitionIds":["122528c2-c6e9-49a1-ad3f-9cb365feaad0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"10"},"parameterTypeName":"int"}]}]}]}} +,{"testCaseStarted":{"attempt":0,"id":"60372689-764d-41d5-bb81-8c82126437e9","testCaseId":"53456c72-c9bc-48f6-9592-b8d78e5f14d3","workerId":"ForkJoinPool-2-worker-4","timestamp":{"seconds":1783364925,"nanos":701163348}}} +,{"testStepStarted":{"testCaseStartedId":"60372689-764d-41d5-bb81-8c82126437e9","testStepId":"47d5e09f-7426-4e14-b7ee-ee78f3849810","timestamp":{"seconds":1783364925,"nanos":702147621}}} +,{"testStepFinished":{"testCaseStartedId":"6f571cd2-3514-41ad-aa35-1afdeb863439","testStepId":"84506d2f-0582-4982-aad1-40a4d08b8987","testStepResult":{"duration":{"seconds":20,"nanos":8562487},"status":"PASSED"},"timestamp":{"seconds":1783364930,"nanos":684667904}}} +,{"testCaseFinished":{"testCaseStartedId":"6f571cd2-3514-41ad-aa35-1afdeb863439","timestamp":{"seconds":1783364930,"nanos":685582964},"willBeRetried":false}} +,{"testStepFinished":{"testCaseStartedId":"f247c885-138a-414e-bcb4-f5a2e3dc053d","testStepId":"c301dac5-6567-41df-abe6-e78ccbb0d494","testStepResult":{"duration":{"seconds":20,"nanos":16704917},"status":"PASSED"},"timestamp":{"seconds":1783364930,"nanos":684674060}}} +,{"testCaseFinished":{"testCaseStartedId":"f247c885-138a-414e-bcb4-f5a2e3dc053d","timestamp":{"seconds":1783364930,"nanos":686985746},"willBeRetried":false}} +,{"testCase":{"id":"5430daee-cea6-4dfc-9daf-230654b96f14","pickleId":"62d538a3-805a-4e77-aa84-2c4cdd23b78a","testSteps":[{"id":"ee0f8afd-2bae-451a-99e6-cf597516abd0","pickleStepId":"9961450c-9f4d-4fda-b0bf-e4990beade63","stepDefinitionIds":["9d239edf-87f2-4821-bfa0-bdff16aba193"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"15"},"parameterTypeName":"int"}]}]}]}} +,{"testCaseStarted":{"attempt":0,"id":"46b79607-b297-44ed-afee-d91deecee9a0","testCaseId":"5430daee-cea6-4dfc-9daf-230654b96f14","workerId":"ForkJoinPool-2-worker-1","timestamp":{"seconds":1783364930,"nanos":690919185}}} +,{"testStepStarted":{"testCaseStartedId":"46b79607-b297-44ed-afee-d91deecee9a0","testStepId":"ee0f8afd-2bae-451a-99e6-cf597516abd0","timestamp":{"seconds":1783364930,"nanos":692260192}}} +,{"testStepFinished":{"testCaseStartedId":"58a3ca27-7b8a-4625-9441-67c937a02c44","testStepId":"6ee6ca02-77de-4d58-9a14-f5d1976c9409","testStepResult":{"duration":{"seconds":15,"nanos":951584},"status":"PASSED"},"timestamp":{"seconds":1783364930,"nanos":706858220}}} +,{"testCaseFinished":{"testCaseStartedId":"58a3ca27-7b8a-4625-9441-67c937a02c44","timestamp":{"seconds":1783364930,"nanos":708050850},"willBeRetried":false}} +,{"testStepFinished":{"testCaseStartedId":"60372689-764d-41d5-bb81-8c82126437e9","testStepId":"47d5e09f-7426-4e14-b7ee-ee78f3849810","testStepResult":{"duration":{"seconds":10,"nanos":1465881},"status":"PASSED"},"timestamp":{"seconds":1783364935,"nanos":703613502}}} +,{"testCaseFinished":{"testCaseStartedId":"60372689-764d-41d5-bb81-8c82126437e9","timestamp":{"seconds":1783364935,"nanos":704725162},"willBeRetried":false}} +,{"testStepFinished":{"testCaseStartedId":"46b79607-b297-44ed-afee-d91deecee9a0","testStepId":"ee0f8afd-2bae-451a-99e6-cf597516abd0","testStepResult":{"duration":{"seconds":15,"nanos":1091989},"status":"PASSED"},"timestamp":{"seconds":1783364945,"nanos":693352181}}} +,{"testCaseFinished":{"testCaseStartedId":"46b79607-b297-44ed-afee-d91deecee9a0","timestamp":{"seconds":1783364945,"nanos":694007340},"willBeRetried":false}} +,{"testRunFinished":{"success":true,"timestamp":{"seconds":1783364945,"nanos":699080137}}} +] as ReadonlyArray From 61a3d5003f9965fc67c786845801cbd9d9fc9130 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Thu, 9 Jul 2026 16:15:31 +0500 Subject: [PATCH 08/26] removed old parallel-run file (locally generated) --- samples/parallel-run.ts | 63 ----------------------------------------- 1 file changed, 63 deletions(-) delete mode 100644 samples/parallel-run.ts diff --git a/samples/parallel-run.ts b/samples/parallel-run.ts deleted file mode 100644 index 7032c134..00000000 --- a/samples/parallel-run.ts +++ /dev/null @@ -1,63 +0,0 @@ -import type { Envelope } from '@cucumber/messages' - -export default [ - {"meta":{"protocolVersion":"29.0.1","implementation":{"name":"cucumber-jvm","version":"7.30.0"},"runtime":{"name":"Java HotSpot(TM) 64-Bit Server VM","version":"26.0.1+8-34"},"os":{"name":"Linux"},"cpu":{"name":"amd64"}}} -,{"testRunStarted":{"timestamp":{"seconds":1783364910,"nanos":419492005}}} -,{"source":{"uri":"classpath:parallel/scenarios.feature","data":"Feature: Parallel Scenarios\n\n Scenario: One\n Given I wait for 5 seconds\n\n Scenario: Two\n Given I wait for 10 seconds\n\n Scenario: Three\n Given I wait for 15 seconds\n\n Scenario: Four\n Given I wait for 20 seconds","mediaType":"text/x.cucumber.gherkin+plain"}} -,{"gherkinDocument":{"uri":"classpath:parallel/scenarios.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en","keyword":"Feature","name":"Parallel Scenarios","description":"","children":[{"scenario":{"location":{"line":3,"column":3},"tags":[],"keyword":"Scenario","name":"One","description":"","steps":[{"location":{"line":4,"column":5},"keyword":"Given ","keywordType":"Context","text":"I wait for 5 seconds","id":"ec7dd26c-eff5-4987-9e49-e4d6ebc47367"}],"examples":[],"id":"5ecdab5f-6f2e-4dbc-99bd-5d34087df0db"}},{"scenario":{"location":{"line":6,"column":3},"tags":[],"keyword":"Scenario","name":"Two","description":"","steps":[{"location":{"line":7,"column":5},"keyword":"Given ","keywordType":"Context","text":"I wait for 10 seconds","id":"aad30de0-0f9d-4aee-aab6-4c9b89d2dc82"}],"examples":[],"id":"9ec6892a-20d6-4179-bc9b-0a5ee5352fcf"}},{"scenario":{"location":{"line":9,"column":3},"tags":[],"keyword":"Scenario","name":"Three","description":"","steps":[{"location":{"line":10,"column":5},"keyword":"Given ","keywordType":"Context","text":"I wait for 15 seconds","id":"27fb1e9b-2da9-44df-9a40-3b9cd5f255dd"}],"examples":[],"id":"73fddbfe-bed6-4b96-b134-2e40bcbcbe77"}},{"scenario":{"location":{"line":12,"column":3},"tags":[],"keyword":"Scenario","name":"Four","description":"","steps":[{"location":{"line":13,"column":5},"keyword":"Given ","keywordType":"Context","text":"I wait for 20 seconds","id":"23b79b90-7ae6-4614-8790-c5b01ed66d9e"}],"examples":[],"id":"d2e50951-bc48-4c28-b513-0e395ac0a146"}}]},"comments":[]}} -,{"pickle":{"id":"8f92f4e6-d3a5-4db6-bfc7-ef4cd7ab7104","uri":"classpath:parallel/scenarios.feature","name":"One","language":"en","steps":[{"astNodeIds":["ec7dd26c-eff5-4987-9e49-e4d6ebc47367"],"id":"5f9baeb1-a1c0-4e58-9dbc-83e64836c83c","type":"Context","text":"I wait for 5 seconds"}],"tags":[],"astNodeIds":["5ecdab5f-6f2e-4dbc-99bd-5d34087df0db"]}} -,{"pickle":{"id":"ab4838f7-ff58-4ac6-885a-f9532b218146","uri":"classpath:parallel/scenarios.feature","name":"Two","language":"en","steps":[{"astNodeIds":["aad30de0-0f9d-4aee-aab6-4c9b89d2dc82"],"id":"ae6e653e-d568-4c33-a99d-0653393bf03a","type":"Context","text":"I wait for 10 seconds"}],"tags":[],"astNodeIds":["9ec6892a-20d6-4179-bc9b-0a5ee5352fcf"]}} -,{"pickle":{"id":"742927e4-043d-404c-aea3-83edf921a0aa","uri":"classpath:parallel/scenarios.feature","name":"Three","language":"en","steps":[{"astNodeIds":["27fb1e9b-2da9-44df-9a40-3b9cd5f255dd"],"id":"a96d5267-582e-4857-9d96-6816dc3e4735","type":"Context","text":"I wait for 15 seconds"}],"tags":[],"astNodeIds":["73fddbfe-bed6-4b96-b134-2e40bcbcbe77"]}} -,{"pickle":{"id":"90869e14-d1a1-46f8-b959-5e8d937772dd","uri":"classpath:parallel/scenarios.feature","name":"Four","language":"en","steps":[{"astNodeIds":["23b79b90-7ae6-4614-8790-c5b01ed66d9e"],"id":"9ed17bf5-1c87-4dba-b3a4-0f7195efa9e7","type":"Context","text":"I wait for 20 seconds"}],"tags":[],"astNodeIds":["d2e50951-bc48-4c28-b513-0e395ac0a146"]}} -,{"source":{"uri":"classpath:parallel/scenario-outlines.feature","data":"Feature: Scenario Outline\n\n Scenario Outline: Waiting\n\n Given I wait for seconds\n\n Examples:\n | seconds |\n | 5 |\n | 10 |\n | 15 |\n | 20 |","mediaType":"text/x.cucumber.gherkin+plain"}} -,{"gherkinDocument":{"uri":"classpath:parallel/scenario-outlines.feature","feature":{"location":{"line":1,"column":1},"tags":[],"language":"en","keyword":"Feature","name":"Scenario Outline","description":"","children":[{"scenario":{"location":{"line":3,"column":3},"tags":[],"keyword":"Scenario Outline","name":"Waiting","description":"","steps":[{"location":{"line":5,"column":5},"keyword":"Given ","keywordType":"Context","text":"I wait for seconds","id":"9939e61a-a7ad-4164-b84a-73d97e0e1c0b"}],"examples":[{"location":{"line":7,"column":5},"tags":[],"keyword":"Examples","name":"","description":"","tableHeader":{"location":{"line":8,"column":7},"cells":[{"location":{"line":8,"column":9},"value":"seconds"}],"id":"4b96e4fe-035a-4781-9663-75312d646d6b"},"tableBody":[{"location":{"line":9,"column":7},"cells":[{"location":{"line":9,"column":9},"value":"5"}],"id":"4946302f-b4cf-4e73-9eed-634a9a099c55"},{"location":{"line":10,"column":7},"cells":[{"location":{"line":10,"column":9},"value":"10"}],"id":"a640ecbd-23b0-469d-bf24-4b72e45d5812"},{"location":{"line":11,"column":7},"cells":[{"location":{"line":11,"column":9},"value":"15"}],"id":"05a33b74-9aa0-412c-bcee-abcc26343512"},{"location":{"line":12,"column":7},"cells":[{"location":{"line":12,"column":9},"value":"20"}],"id":"3fbf7a14-5518-4518-bb90-0ac06dcf6aff"}],"id":"f250783f-e983-484d-af73-981bb08fed50"}],"id":"95e218da-cab5-4fbf-beea-6558b4747cdb"}}]},"comments":[]}} -,{"pickle":{"id":"5d6d82e8-a595-4263-844e-6f16accdc8df","uri":"classpath:parallel/scenario-outlines.feature","name":"Waiting","language":"en","steps":[{"astNodeIds":["9939e61a-a7ad-4164-b84a-73d97e0e1c0b","4946302f-b4cf-4e73-9eed-634a9a099c55"],"id":"fb4414fa-e52d-4fbe-9b8c-eff368dc72b5","type":"Context","text":"I wait for 5 seconds"}],"tags":[],"astNodeIds":["95e218da-cab5-4fbf-beea-6558b4747cdb","4946302f-b4cf-4e73-9eed-634a9a099c55"]}} -,{"pickle":{"id":"3986b982-1a48-4774-9ada-8a9e0349aa02","uri":"classpath:parallel/scenario-outlines.feature","name":"Waiting","language":"en","steps":[{"astNodeIds":["9939e61a-a7ad-4164-b84a-73d97e0e1c0b","a640ecbd-23b0-469d-bf24-4b72e45d5812"],"id":"b0fecc18-4e3f-432f-8182-6449940ce593","type":"Context","text":"I wait for 10 seconds"}],"tags":[],"astNodeIds":["95e218da-cab5-4fbf-beea-6558b4747cdb","a640ecbd-23b0-469d-bf24-4b72e45d5812"]}} -,{"pickle":{"id":"62d538a3-805a-4e77-aa84-2c4cdd23b78a","uri":"classpath:parallel/scenario-outlines.feature","name":"Waiting","language":"en","steps":[{"astNodeIds":["9939e61a-a7ad-4164-b84a-73d97e0e1c0b","05a33b74-9aa0-412c-bcee-abcc26343512"],"id":"9961450c-9f4d-4fda-b0bf-e4990beade63","type":"Context","text":"I wait for 15 seconds"}],"tags":[],"astNodeIds":["95e218da-cab5-4fbf-beea-6558b4747cdb","05a33b74-9aa0-412c-bcee-abcc26343512"]}} -,{"pickle":{"id":"8c7958a6-f6cb-4798-8f0f-7db60a263f1e","uri":"classpath:parallel/scenario-outlines.feature","name":"Waiting","language":"en","steps":[{"astNodeIds":["9939e61a-a7ad-4164-b84a-73d97e0e1c0b","3fbf7a14-5518-4518-bb90-0ac06dcf6aff"],"id":"1f45b7e6-f3ab-435c-80bd-a8dee0219b76","type":"Context","text":"I wait for 20 seconds"}],"tags":[],"astNodeIds":["95e218da-cab5-4fbf-beea-6558b4747cdb","3fbf7a14-5518-4518-bb90-0ac06dcf6aff"]}} -,{"stepDefinition":{"id":"122528c2-c6e9-49a1-ad3f-9cb365feaad0","pattern":{"source":"I wait for {int} seconds","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"parallel.StepDefinitions","methodName":"waitFor","methodParameterTypes":["int"]}}}} -,{"stepDefinition":{"id":"9d239edf-87f2-4821-bfa0-bdff16aba193","pattern":{"source":"I wait for {int} seconds","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"parallel.StepDefinitions","methodName":"waitFor","methodParameterTypes":["int"]}}}} -,{"stepDefinition":{"id":"c582c113-0461-4f5c-a1bd-db373063bfd4","pattern":{"source":"I wait for {int} seconds","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"parallel.StepDefinitions","methodName":"waitFor","methodParameterTypes":["int"]}}}} -,{"stepDefinition":{"id":"0b938f0d-a79d-4d4a-90c5-51e72abb8160","pattern":{"source":"I wait for {int} seconds","type":"CUCUMBER_EXPRESSION"},"sourceReference":{"javaMethod":{"className":"parallel.StepDefinitions","methodName":"waitFor","methodParameterTypes":["int"]}}}} -,{"testCase":{"id":"e191b6b7-a04b-4d30-a23b-58b05df68c89","pickleId":"ab4838f7-ff58-4ac6-885a-f9532b218146","testSteps":[{"id":"e164c9fb-06e3-49a5-8725-a9a8b2e9c28c","pickleStepId":"ae6e653e-d568-4c33-a99d-0653393bf03a","stepDefinitionIds":["122528c2-c6e9-49a1-ad3f-9cb365feaad0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"10"},"parameterTypeName":"int"}]}]}]}} -,{"testCase":{"id":"7c14d6c9-e1b0-4f90-8264-6c671b1f69c4","pickleId":"8c7958a6-f6cb-4798-8f0f-7db60a263f1e","testSteps":[{"id":"c301dac5-6567-41df-abe6-e78ccbb0d494","pickleStepId":"1f45b7e6-f3ab-435c-80bd-a8dee0219b76","stepDefinitionIds":["c582c113-0461-4f5c-a1bd-db373063bfd4"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"20"},"parameterTypeName":"int"}]}]}]}} -,{"testCaseStarted":{"attempt":0,"id":"f247c885-138a-414e-bcb4-f5a2e3dc053d","testCaseId":"7c14d6c9-e1b0-4f90-8264-6c671b1f69c4","workerId":"ForkJoinPool-2-worker-2","timestamp":{"seconds":1783364910,"nanos":655349506}}} -,{"testStepStarted":{"testCaseStartedId":"f247c885-138a-414e-bcb4-f5a2e3dc053d","testStepId":"c301dac5-6567-41df-abe6-e78ccbb0d494","timestamp":{"seconds":1783364910,"nanos":667969143}}} -,{"testCase":{"id":"6f9693a3-16a1-46ee-ba0e-62875dad40fe","pickleId":"90869e14-d1a1-46f8-b959-5e8d937772dd","testSteps":[{"id":"84506d2f-0582-4982-aad1-40a4d08b8987","pickleStepId":"9ed17bf5-1c87-4dba-b3a4-0f7195efa9e7","stepDefinitionIds":["9d239edf-87f2-4821-bfa0-bdff16aba193"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"20"},"parameterTypeName":"int"}]}]}]}} -,{"testCaseStarted":{"attempt":0,"id":"6f571cd2-3514-41ad-aa35-1afdeb863439","testCaseId":"6f9693a3-16a1-46ee-ba0e-62875dad40fe","workerId":"ForkJoinPool-2-worker-1","timestamp":{"seconds":1783364910,"nanos":674377251}}} -,{"testStepStarted":{"testCaseStartedId":"6f571cd2-3514-41ad-aa35-1afdeb863439","testStepId":"84506d2f-0582-4982-aad1-40a4d08b8987","timestamp":{"seconds":1783364910,"nanos":676105417}}} -,{"testCase":{"id":"e5b2bec6-6f10-4e17-954c-5686cf8f8d63","pickleId":"8f92f4e6-d3a5-4db6-bfc7-ef4cd7ab7104","testSteps":[{"id":"d7dfbbeb-a408-414a-813f-f026b4a7fdd0","pickleStepId":"5f9baeb1-a1c0-4e58-9dbc-83e64836c83c","stepDefinitionIds":["0b938f0d-a79d-4d4a-90c5-51e72abb8160"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"5"},"parameterTypeName":"int"}]}]}]}} -,{"testCaseStarted":{"attempt":0,"id":"0d58f229-68a7-471d-bdc2-4333844acf0b","testCaseId":"e5b2bec6-6f10-4e17-954c-5686cf8f8d63","workerId":"ForkJoinPool-2-worker-3","timestamp":{"seconds":1783364910,"nanos":677951850}}} -,{"testCaseStarted":{"attempt":0,"id":"f623c1cb-89a0-4a50-b279-341512fd4d09","testCaseId":"e191b6b7-a04b-4d30-a23b-58b05df68c89","workerId":"ForkJoinPool-2-worker-4","timestamp":{"seconds":1783364910,"nanos":653898230}}} -,{"testStepStarted":{"testCaseStartedId":"0d58f229-68a7-471d-bdc2-4333844acf0b","testStepId":"d7dfbbeb-a408-414a-813f-f026b4a7fdd0","timestamp":{"seconds":1783364910,"nanos":679224081}}} -,{"testStepStarted":{"testCaseStartedId":"f623c1cb-89a0-4a50-b279-341512fd4d09","testStepId":"e164c9fb-06e3-49a5-8725-a9a8b2e9c28c","timestamp":{"seconds":1783364910,"nanos":680352541}}} -,{"testStepFinished":{"testCaseStartedId":"0d58f229-68a7-471d-bdc2-4333844acf0b","testStepId":"d7dfbbeb-a408-414a-813f-f026b4a7fdd0","testStepResult":{"duration":{"seconds":5,"nanos":7209271},"status":"PASSED"},"timestamp":{"seconds":1783364915,"nanos":686433352}}} -,{"testCaseFinished":{"testCaseStartedId":"0d58f229-68a7-471d-bdc2-4333844acf0b","timestamp":{"seconds":1783364915,"nanos":694477859},"willBeRetried":false}} -,{"testCase":{"id":"714fa141-779b-44b5-9e1a-ac7eca676d2e","pickleId":"742927e4-043d-404c-aea3-83edf921a0aa","testSteps":[{"id":"6ee6ca02-77de-4d58-9a14-f5d1976c9409","pickleStepId":"a96d5267-582e-4857-9d96-6816dc3e4735","stepDefinitionIds":["0b938f0d-a79d-4d4a-90c5-51e72abb8160"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"15"},"parameterTypeName":"int"}]}]}]}} -,{"testCaseStarted":{"attempt":0,"id":"58a3ca27-7b8a-4625-9441-67c937a02c44","testCaseId":"714fa141-779b-44b5-9e1a-ac7eca676d2e","workerId":"ForkJoinPool-2-worker-3","timestamp":{"seconds":1783364915,"nanos":704863171}}} -,{"testStepStarted":{"testCaseStartedId":"58a3ca27-7b8a-4625-9441-67c937a02c44","testStepId":"6ee6ca02-77de-4d58-9a14-f5d1976c9409","timestamp":{"seconds":1783364915,"nanos":705906636}}} -,{"testStepFinished":{"testCaseStartedId":"f623c1cb-89a0-4a50-b279-341512fd4d09","testStepId":"e164c9fb-06e3-49a5-8725-a9a8b2e9c28c","testStepResult":{"duration":{"seconds":10,"nanos":4022840},"status":"PASSED"},"timestamp":{"seconds":1783364920,"nanos":684375381}}} -,{"testCaseFinished":{"testCaseStartedId":"f623c1cb-89a0-4a50-b279-341512fd4d09","timestamp":{"seconds":1783364920,"nanos":685612826},"willBeRetried":false}} -,{"testCase":{"id":"709a4ecb-3b44-42de-a2dd-438c643f8cbf","pickleId":"5d6d82e8-a595-4263-844e-6f16accdc8df","testSteps":[{"id":"c023eb48-deee-4058-88de-7caf5ed11a1b","pickleStepId":"fb4414fa-e52d-4fbe-9b8c-eff368dc72b5","stepDefinitionIds":["122528c2-c6e9-49a1-ad3f-9cb365feaad0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"5"},"parameterTypeName":"int"}]}]}]}} -,{"testCaseStarted":{"attempt":0,"id":"6f6868b3-d003-4d37-9485-b559f70cdfd5","testCaseId":"709a4ecb-3b44-42de-a2dd-438c643f8cbf","workerId":"ForkJoinPool-2-worker-4","timestamp":{"seconds":1783364920,"nanos":691156055}}} -,{"testStepStarted":{"testCaseStartedId":"6f6868b3-d003-4d37-9485-b559f70cdfd5","testStepId":"c023eb48-deee-4058-88de-7caf5ed11a1b","timestamp":{"seconds":1783364920,"nanos":692589594}}} -,{"testStepFinished":{"testCaseStartedId":"6f6868b3-d003-4d37-9485-b559f70cdfd5","testStepId":"c023eb48-deee-4058-88de-7caf5ed11a1b","testStepResult":{"duration":{"seconds":5,"nanos":1367400},"status":"PASSED"},"timestamp":{"seconds":1783364925,"nanos":693956994}}} -,{"testCaseFinished":{"testCaseStartedId":"6f6868b3-d003-4d37-9485-b559f70cdfd5","timestamp":{"seconds":1783364925,"nanos":695212572},"willBeRetried":false}} -,{"testCase":{"id":"53456c72-c9bc-48f6-9592-b8d78e5f14d3","pickleId":"3986b982-1a48-4774-9ada-8a9e0349aa02","testSteps":[{"id":"47d5e09f-7426-4e14-b7ee-ee78f3849810","pickleStepId":"b0fecc18-4e3f-432f-8182-6449940ce593","stepDefinitionIds":["122528c2-c6e9-49a1-ad3f-9cb365feaad0"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"10"},"parameterTypeName":"int"}]}]}]}} -,{"testCaseStarted":{"attempt":0,"id":"60372689-764d-41d5-bb81-8c82126437e9","testCaseId":"53456c72-c9bc-48f6-9592-b8d78e5f14d3","workerId":"ForkJoinPool-2-worker-4","timestamp":{"seconds":1783364925,"nanos":701163348}}} -,{"testStepStarted":{"testCaseStartedId":"60372689-764d-41d5-bb81-8c82126437e9","testStepId":"47d5e09f-7426-4e14-b7ee-ee78f3849810","timestamp":{"seconds":1783364925,"nanos":702147621}}} -,{"testStepFinished":{"testCaseStartedId":"6f571cd2-3514-41ad-aa35-1afdeb863439","testStepId":"84506d2f-0582-4982-aad1-40a4d08b8987","testStepResult":{"duration":{"seconds":20,"nanos":8562487},"status":"PASSED"},"timestamp":{"seconds":1783364930,"nanos":684667904}}} -,{"testCaseFinished":{"testCaseStartedId":"6f571cd2-3514-41ad-aa35-1afdeb863439","timestamp":{"seconds":1783364930,"nanos":685582964},"willBeRetried":false}} -,{"testStepFinished":{"testCaseStartedId":"f247c885-138a-414e-bcb4-f5a2e3dc053d","testStepId":"c301dac5-6567-41df-abe6-e78ccbb0d494","testStepResult":{"duration":{"seconds":20,"nanos":16704917},"status":"PASSED"},"timestamp":{"seconds":1783364930,"nanos":684674060}}} -,{"testCaseFinished":{"testCaseStartedId":"f247c885-138a-414e-bcb4-f5a2e3dc053d","timestamp":{"seconds":1783364930,"nanos":686985746},"willBeRetried":false}} -,{"testCase":{"id":"5430daee-cea6-4dfc-9daf-230654b96f14","pickleId":"62d538a3-805a-4e77-aa84-2c4cdd23b78a","testSteps":[{"id":"ee0f8afd-2bae-451a-99e6-cf597516abd0","pickleStepId":"9961450c-9f4d-4fda-b0bf-e4990beade63","stepDefinitionIds":["9d239edf-87f2-4821-bfa0-bdff16aba193"],"stepMatchArgumentsLists":[{"stepMatchArguments":[{"group":{"children":[],"start":11,"value":"15"},"parameterTypeName":"int"}]}]}]}} -,{"testCaseStarted":{"attempt":0,"id":"46b79607-b297-44ed-afee-d91deecee9a0","testCaseId":"5430daee-cea6-4dfc-9daf-230654b96f14","workerId":"ForkJoinPool-2-worker-1","timestamp":{"seconds":1783364930,"nanos":690919185}}} -,{"testStepStarted":{"testCaseStartedId":"46b79607-b297-44ed-afee-d91deecee9a0","testStepId":"ee0f8afd-2bae-451a-99e6-cf597516abd0","timestamp":{"seconds":1783364930,"nanos":692260192}}} -,{"testStepFinished":{"testCaseStartedId":"58a3ca27-7b8a-4625-9441-67c937a02c44","testStepId":"6ee6ca02-77de-4d58-9a14-f5d1976c9409","testStepResult":{"duration":{"seconds":15,"nanos":951584},"status":"PASSED"},"timestamp":{"seconds":1783364930,"nanos":706858220}}} -,{"testCaseFinished":{"testCaseStartedId":"58a3ca27-7b8a-4625-9441-67c937a02c44","timestamp":{"seconds":1783364930,"nanos":708050850},"willBeRetried":false}} -,{"testStepFinished":{"testCaseStartedId":"60372689-764d-41d5-bb81-8c82126437e9","testStepId":"47d5e09f-7426-4e14-b7ee-ee78f3849810","testStepResult":{"duration":{"seconds":10,"nanos":1465881},"status":"PASSED"},"timestamp":{"seconds":1783364935,"nanos":703613502}}} -,{"testCaseFinished":{"testCaseStartedId":"60372689-764d-41d5-bb81-8c82126437e9","timestamp":{"seconds":1783364935,"nanos":704725162},"willBeRetried":false}} -,{"testStepFinished":{"testCaseStartedId":"46b79607-b297-44ed-afee-d91deecee9a0","testStepId":"ee0f8afd-2bae-451a-99e6-cf597516abd0","testStepResult":{"duration":{"seconds":15,"nanos":1091989},"status":"PASSED"},"timestamp":{"seconds":1783364945,"nanos":693352181}}} -,{"testCaseFinished":{"testCaseStartedId":"46b79607-b297-44ed-afee-d91deecee9a0","timestamp":{"seconds":1783364945,"nanos":694007340},"willBeRetried":false}} -,{"testRunFinished":{"success":true,"timestamp":{"seconds":1783364945,"nanos":699080137}}} -] as ReadonlyArray From 9cb7ea261eacca8da7ff40f6464ce4aa0dfc2e4c Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Thu, 9 Jul 2026 18:35:06 +0500 Subject: [PATCH 09/26] Reimplemented Timeline component using vis-timeline --- package-lock.json | 175 +++++++++++++++++++++--- package.json | 4 +- src/components/app/Timeline.module.scss | 116 ++-------------- src/components/app/Timeline.stories.tsx | 43 +----- src/components/app/Timeline.tsx | 154 ++++++++++----------- src/custom.d.ts | 2 + src/hooks/useTimelineData.ts | 27 ++-- 7 files changed, 271 insertions(+), 250 deletions(-) diff --git a/package-lock.json b/package-lock.json index 10d6a7a1..fe74a55a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,7 +31,9 @@ "rehype-sanitize": "6.0.0", "remark-breaks": "4.0.0", "remark-gfm": "4.0.1", - "use-debounce": "^10.0.0" + "use-debounce": "^10.0.0", + "vis-data": "^8.0.4", + "vis-timeline": "^8.5.1" }, "devDependencies": { "@biomejs/biome": "^2.4.1", @@ -924,6 +926,19 @@ "integrity": "sha512-uap3XSQFxj5HYAHQIShGeS2zotMEnUmnEVjyuhp39j7tDUvaU64ArHzRkLPSWRavEI8ycdeNdwef1pcM/n6pSQ==", "license": "MIT" }, + "node_modules/@egjs/hammerjs": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", + "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", + "license": "MIT", + "peer": true, + "dependencies": { + "@types/hammerjs": "^2.0.36" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", @@ -5266,6 +5281,13 @@ "glob": "*" } }, + "node_modules/@types/hammerjs": { + "version": "2.0.46", + "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", + "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==", + "license": "MIT", + "peer": true + }, "node_modules/@types/highlight-words-core": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/@types/highlight-words-core/-/highlight-words-core-1.2.3.tgz", @@ -6227,6 +6249,16 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, + "node_modules/component-emitter": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", + "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -6440,6 +6472,13 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, + "node_modules/cssfilter": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", + "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", + "license": "MIT", + "peer": true + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -8884,6 +8923,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/keycharm": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", + "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==", + "license": "(Apache-2.0 OR MIT)", + "peer": true + }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -10891,6 +10937,16 @@ "node": ">=12" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "license": "MIT", + "peer": true, + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -11697,6 +11753,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, + "node_modules/propagating-hammerjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/propagating-hammerjs/-/propagating-hammerjs-3.0.0.tgz", + "integrity": "sha512-FJTclGll0ysatpF9rKO4jwobyaVDitPb0g/bGlufqqtXPQX8mxf8IXilnIK2iYRMPkVlYeFNhPTrspF9CM1stg==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.17" + } + }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -13671,6 +13737,20 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -13687,6 +13767,59 @@ "node": ">= 0.8" } }, + "node_modules/vis-data": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-8.0.4.tgz", + "integrity": "sha512-TsN0sMHqIRpdfg6TNPtfdINpkgxtnQP6JNWCaiSwvou5seXqKiP5eERkaBg+Y56wyJ4FZTeOEs/dEmWEPrpltQ==", + "license": "(Apache-2.0 OR MIT)", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^13.0.0 || ^14.0.0", + "vis-util": ">=6.0.0" + } + }, + "node_modules/vis-timeline": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/vis-timeline/-/vis-timeline-8.5.1.tgz", + "integrity": "sha512-6pqx4Zl/xHCEy5nXRaz9xCOx1HtZWkxSIt1oHJmDZy/UxT09kwq1eA4eKRAzhHey3PN1Ee3DsxuxHm7yI2G2mQ==", + "license": "(Apache-2.0 OR MIT)", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0", + "keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0", + "moment": "^2.24.0", + "propagating-hammerjs": "^1.4.0 || ^2.0.0 || ^3.0.0", + "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^13.0.0 || ^14.0.0", + "vis-data": ">=8.0.0", + "vis-util": ">=6.0.0", + "xss": "^1.0.0" + } + }, + "node_modules/vis-util": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-6.0.0.tgz", + "integrity": "sha512-qtpts3HRma0zPe4bO7t9A2uejkRNj8Z2Tb6do6lN85iPNWExFkUiVhdAq5uLGIUqBFduyYeqWJKv/jMkxX0R5g==", + "license": "(Apache-2.0 OR MIT)", + "peer": true, + "engines": { + "node": ">=8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/visjs" + }, + "peerDependencies": { + "@egjs/hammerjs": "^2.0.0", + "component-emitter": "^1.3.0 || ^2.0.0" + } + }, "node_modules/vite": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", @@ -13803,22 +13936,6 @@ } } }, - "node_modules/vite-tsconfig-paths/node_modules/typescript": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/vite/node_modules/fdir": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", @@ -14090,6 +14207,30 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/xss": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", + "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", + "license": "MIT", + "peer": true, + "dependencies": { + "commander": "^2.20.3", + "cssfilter": "0.0.10" + }, + "bin": { + "xss": "bin/xss" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/xss/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT", + "peer": true + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 5a273935..4dbd079a 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,9 @@ "rehype-sanitize": "6.0.0", "remark-breaks": "4.0.0", "remark-gfm": "4.0.1", - "use-debounce": "^10.0.0" + "use-debounce": "^10.0.0", + "vis-data": "^8.0.4", + "vis-timeline": "^8.5.1" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", diff --git a/src/components/app/Timeline.module.scss b/src/components/app/Timeline.module.scss index 3cb8622b..a8e4f64c 100644 --- a/src/components/app/Timeline.module.scss +++ b/src/components/app/Timeline.module.scss @@ -2,123 +2,25 @@ @use '../../styles/theming'; .container { - display: flex; - flex-direction: column; - gap: 1em; -} - -.empty { - font-style: italic; -} - -.chart { - position: relative; - overflow-x: auto; + border: 1px solid theming.$panelAccentColor; } -.axis { - position: relative; - height: 1.5em; - margin: 0 0 0.5em; - padding: 0; - list-style: none; - border-bottom: 1px solid theming.$panelAccentColor; - min-width: 40em; -} - -.tick { - position: absolute; - top: 0; - bottom: 0; - border-left: 1px dashed theming.$panelAccentColor; - padding-left: 0.35em; - font-size: 0.75em; - opacity: 0.75; - white-space: nowrap; - - &[data-edge='end'] { - border-left: none; - border-right: 1px dashed theming.$panelAccentColor; - padding-left: 0; - padding-right: 0.35em; - text-align: right; - - span { - display: inline-block; - transform: translateX(-100%); - } +@each $name, $color in statuses.$statusColors { + .visItem[data-status='#{$name}'] { + background-color: $color; + border-color: $color; } -} - -.groups { - display: flex; - flex-direction: column; - gap: 0.25em; - padding: 0; - margin: 0; - list-style: none; - min-width: 40em; -} -.group { - display: flex; - align-items: center; - gap: 0.5em; -} - -.groupLabel { - flex: 0 0 auto; - width: 8em; - font-size: 0.85em; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - opacity: 0.75; -} +.visItem[data-status='#{$name}']:global(.vis-selected) { + background-color: $color; + border-color: $color; -.lane { - position: relative; - flex: 1 1 auto; - height: 2.25em; - background-color: theming.$panelBackgroundColor; - border-radius: 0.25em; -} - -.item { - position: absolute; - top: 0.25em; - bottom: 0.25em; - min-width: 6px; - padding: 0 0.4em; - overflow: hidden; - border: none; - border-radius: 0.2em; - cursor: pointer; - color: white; - font: inherit; - font-size: 0.75em; - text-align: left; - - @each $name, $color in statuses.$statusColors { - &[data-status='#{$name}'] { - background-color: $color; - } - } - - &[aria-pressed='true'] { outline: 2px solid theming.$panelTextColor; outline-offset: 1px; - z-index: 1; + z-index: 2; } } -.itemLabel { - display: block; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - .detail { position: relative; padding: 1em; diff --git a/src/components/app/Timeline.stories.tsx b/src/components/app/Timeline.stories.tsx index af99a13f..d746ce5d 100644 --- a/src/components/app/Timeline.stories.tsx +++ b/src/components/app/Timeline.stories.tsx @@ -1,7 +1,7 @@ -import { type Envelope, type TestCaseStarted, TimeConversion } from '@cucumber/messages' +import type { Envelope } from '@cucumber/messages' import type { Story } from '@ladle/react' - -import examplesTablesFeature from '../../../acceptance/examples-tables/examples-tables.js' +import examplesTables from '../../../acceptance/examples-tables/examples-tables.js' +import parallel from '../../../acceptance/parallel/parallel.js' import { EnvelopesProvider } from './EnvelopesProvider.js' import { InMemorySearchProvider } from './InMemorySearchProvider.js' import { Timeline } from './Timeline.js' @@ -26,46 +26,15 @@ const Template: Story = ({ envelopes }) => { export const SingleProcess = Template.bind({}) SingleProcess.args = { - envelopes: examplesTablesFeature, + envelopes: examplesTables, } as TemplateArgs export const Parallel = Template.bind({}) Parallel.args = { - envelopes: distributeAcrossWorkers(examplesTablesFeature, 3), + envelopes: parallel, } as TemplateArgs export const NoTestCases = Template.bind({}) NoTestCases.args = { - envelopes: [ - { testRunStarted: { timestamp: TimeConversion.millisecondsSinceEpochToTimestamp(0) } }, - { - testRunFinished: { - timestamp: TimeConversion.millisecondsSinceEpochToTimestamp(1000), - success: true, - }, - }, - ], + envelopes: [], } as TemplateArgs - -/** - * Cucumber implementations report which worker ran a test case via - * `TestCaseStarted.workerId`. The compatibility-kit fixtures used in this story - * were captured from a single-process run so this helper distributes the - * existing test cases across a number of synthetic workers to demonstrate how - * the timeline renders parallel execution. - */ -function distributeAcrossWorkers( - envelopes: ReadonlyArray, - workerCount: number -): ReadonlyArray { - let index = 0 - return envelopes.map((envelope): Envelope => { - if (!envelope.testCaseStarted) { - return envelope - } - const workerId = String(index % workerCount) - index += 1 - const testCaseStarted: TestCaseStarted = { ...envelope.testCaseStarted, workerId } - return { ...envelope, testCaseStarted } - }) -} diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index e121fc4b..9c501bbe 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -1,19 +1,84 @@ -import { faXmark } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { type FC, useState } from 'react' - +import { type FC, useEffect, useRef, useState } from 'react' +import { DataSet } from 'vis-data' +import { Timeline as VisTimeline } from 'vis-timeline' import { formatExecutionDuration } from '../../formatExecutionDuration.js' import { type TimelineItem, useTimelineData } from '../../hooks/useTimelineData.js' import { StatusIcon } from '../gherkin/StatusIcon.js' import statusName from '../gherkin/statusName.js' import { Tags } from '../gherkin/Tags.js' +import { TestCaseOutcome } from '../results/index.js' import styles from './Timeline.module.scss' -const AXIS_TICKS = 4 +import 'vis-timeline/styles/vis-timeline-graph2d.css' +import { faXmark } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +type DataSetGroup = { + id: string + content: string +} + +type DataSetItem = { + id: string + content: string + group: string + start: Date + end: Date + status: string + className: string +} export const Timeline: FC = () => { - const { groups, items, start, end, filtered } = useTimelineData() - const [selectedId, setSelectedId] = useState() + const { groups, items, fullStart, fullEnd, filtered } = useTimelineData() + const [selectedId, setSelectedId] = useState() + + const containerRef = useRef(null) + + useEffect(() => { + if (!containerRef.current) { + return + } + + const dataSetGroups = new DataSet() + groups.forEach((g) => { + dataSetGroups.add({ id: g.id, content: g.label }) + }) + + const dataSetItems = new DataSet() + items.forEach((i) => { + dataSetItems.add({ + id: i.id, + content: i.scenario, + group: i.groupId, + start: new Date(i.start), + end: new Date(i.end), + status: i.status, + className: styles.visItem, + }) + }) + + const timeline = new VisTimeline(containerRef.current, dataSetItems, dataSetGroups, { + stack: false, + zoomable: true, + moveable: true, + selectable: true, + editable: false, + showCurrentTime: false, + orientation: 'top', + min: fullStart, + max: fullEnd, + start: fullStart, + end: fullEnd, + dataAttributes: ['status'], + }) + + timeline.on('select', (props: { items: string[] }) => { + setSelectedId(props.items[0] ?? undefined) + }) + + return () => { + timeline.destroy() + } + }, [fullStart, fullEnd, groups, items]) if (items.length === 0) { return filtered ? ( @@ -23,56 +88,11 @@ export const Timeline: FC = () => { ) } - const duration = Math.max(end - start, 1) - const ticks = Array.from({ length: AXIS_TICKS + 1 }, (_, index) => { - const offset = (duration / AXIS_TICKS) * index - return { - index, - position: (offset / duration) * 100, - label: formatExecutionDuration(new Date(start), new Date(start + offset)), - } - }) const selectedItem = items.find((item) => item.id === selectedId) return ( -
-
- -
    - {groups.map((group) => ( -
  1. - {group.label} -
    - {items - .filter((item) => item.groupId === group.id) - .map((item) => ( - - setSelectedId((current) => (current === item.id ? undefined : item.id)) - } - /> - ))} -
    -
  2. - ))} -
-
+
+
{selectedItem && ( setSelectedId(undefined)} /> )} @@ -80,31 +100,6 @@ export const Timeline: FC = () => { ) } -const TimelineBar: FC<{ - item: TimelineItem - rangeStart: number - duration: number - selected: boolean - onSelect: () => void -}> = ({ item, rangeStart, duration, selected, onSelect }) => { - const left = ((item.start - rangeStart) / duration) * 100 - const width = Math.max(((item.end - item.start) / duration) * 100, 0.3) - return ( - - ) -} - const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item, onClose }) => { return (
@@ -131,6 +126,7 @@ const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item,
{item.groupLabel}
+
) } diff --git a/src/custom.d.ts b/src/custom.d.ts index e48f665b..14a684e2 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -2,3 +2,5 @@ declare module '*.module.scss' { const classes: { [key: string]: string } export default classes } + +declare module '*.css' diff --git a/src/hooks/useTimelineData.ts b/src/hooks/useTimelineData.ts index 0385a03d..16ef807e 100644 --- a/src/hooks/useTimelineData.ts +++ b/src/hooks/useTimelineData.ts @@ -30,8 +30,8 @@ export interface TimelineGroup { export interface TimelineData { readonly groups: readonly TimelineGroup[] readonly items: readonly TimelineItem[] - readonly start: number - readonly end: number + readonly fullStart: number | undefined + readonly fullEnd: number | undefined readonly filtered: boolean } @@ -45,6 +45,8 @@ export function useTimelineData(): TimelineData { const items: TimelineItem[] = [] const groupIds = new Set() const normalizedSearchTerm = searchTerm?.trim().toLowerCase() + let fullStart: number | undefined + let fullEnd: number | undefined for (const testCaseFinished of cucumberQuery.findAllTestCaseFinished()) { const testCaseStarted = cucumberQuery.findTestCaseStartedBy(testCaseFinished) @@ -56,6 +58,16 @@ export function useTimelineData(): TimelineData { continue } + const itemStart = TimeConversion.timestampToMillisecondsSinceEpoch(testCaseStarted.timestamp) + const itemEnd = TimeConversion.timestampToMillisecondsSinceEpoch(testCaseFinished.timestamp) + + if (fullStart === undefined || itemStart < fullStart) { + fullStart = itemStart + } + if (fullEnd === undefined || itemEnd > fullEnd) { + fullEnd = itemEnd + } + // A test case with no step results at all is considered passed by definition const status = cucumberQuery.findMostSevereTestStepResultBy(testCaseFinished)?.status ?? @@ -93,8 +105,8 @@ export function useTimelineData(): TimelineData { scenario, tags: pickle.tags, status, - start: TimeConversion.timestampToMillisecondsSinceEpoch(testCaseStarted.timestamp), - end: TimeConversion.timestampToMillisecondsSinceEpoch(testCaseFinished.timestamp), + start: itemStart, + end: itemEnd, testCaseStarted, }) } @@ -105,15 +117,12 @@ export function useTimelineData(): TimelineData { .sort(compareGroupIds) .map((id) => ({ id, label: describeGroup(id) })) - const start = items.length > 0 ? Math.min(...items.map((item) => item.start)) : 0 - const end = items.length > 0 ? Math.max(...items.map((item) => item.end)) : 0 - - return { groups, items, start, end, filtered: !unchanged } + return { groups, items, fullStart, fullEnd, filtered: !unchanged } }, [cucumberQuery, hideStatuses, tagExpression, searchTerm, unchanged]) } function describeGroup(id: string): string { - return id === UNASSIGNED_GROUP_ID ? 'Main process' : `Worker ${id}` + return id === UNASSIGNED_GROUP_ID ? '' : `Worker ${id}` } function compareGroupIds(a: string, b: string): number { From 29e3fed1213d2d169ae4ea8813d370698b691a2d Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Sun, 26 Jul 2026 00:14:56 +0500 Subject: [PATCH 10/26] Added Tooltip --- src/components/app/CustomTimeline.tsx | 132 ++++++++++++++++++++ src/components/app/Timeline.module.scss | 61 +++++++++ src/components/app/Timeline.tsx | 157 ++++++++++++------------ src/hooks/useFilteredTestCases.spec.tsx | 4 +- src/hooks/useFilteredTestCases.ts | 1 + src/hooks/useTimelineData.ts | 49 ++++---- 6 files changed, 302 insertions(+), 102 deletions(-) create mode 100644 src/components/app/CustomTimeline.tsx diff --git a/src/components/app/CustomTimeline.tsx b/src/components/app/CustomTimeline.tsx new file mode 100644 index 00000000..9c501bbe --- /dev/null +++ b/src/components/app/CustomTimeline.tsx @@ -0,0 +1,132 @@ +import { type FC, useEffect, useRef, useState } from 'react' +import { DataSet } from 'vis-data' +import { Timeline as VisTimeline } from 'vis-timeline' +import { formatExecutionDuration } from '../../formatExecutionDuration.js' +import { type TimelineItem, useTimelineData } from '../../hooks/useTimelineData.js' +import { StatusIcon } from '../gherkin/StatusIcon.js' +import statusName from '../gherkin/statusName.js' +import { Tags } from '../gherkin/Tags.js' +import { TestCaseOutcome } from '../results/index.js' +import styles from './Timeline.module.scss' + +import 'vis-timeline/styles/vis-timeline-graph2d.css' +import { faXmark } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' + +type DataSetGroup = { + id: string + content: string +} + +type DataSetItem = { + id: string + content: string + group: string + start: Date + end: Date + status: string + className: string +} +export const Timeline: FC = () => { + const { groups, items, fullStart, fullEnd, filtered } = useTimelineData() + const [selectedId, setSelectedId] = useState() + + const containerRef = useRef(null) + + useEffect(() => { + if (!containerRef.current) { + return + } + + const dataSetGroups = new DataSet() + groups.forEach((g) => { + dataSetGroups.add({ id: g.id, content: g.label }) + }) + + const dataSetItems = new DataSet() + items.forEach((i) => { + dataSetItems.add({ + id: i.id, + content: i.scenario, + group: i.groupId, + start: new Date(i.start), + end: new Date(i.end), + status: i.status, + className: styles.visItem, + }) + }) + + const timeline = new VisTimeline(containerRef.current, dataSetItems, dataSetGroups, { + stack: false, + zoomable: true, + moveable: true, + selectable: true, + editable: false, + showCurrentTime: false, + orientation: 'top', + min: fullStart, + max: fullEnd, + start: fullStart, + end: fullEnd, + dataAttributes: ['status'], + }) + + timeline.on('select', (props: { items: string[] }) => { + setSelectedId(props.items[0] ?? undefined) + }) + + return () => { + timeline.destroy() + } + }, [fullStart, fullEnd, groups, items]) + + if (items.length === 0) { + return filtered ? ( +

No scenarios match your query and/or filters.

+ ) : ( +

No scenarios were executed.

+ ) + } + + const selectedItem = items.find((item) => item.id === selectedId) + + return ( +
+
+ {selectedItem && ( + setSelectedId(undefined)} /> + )} +
+ ) +} + +const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item, onClose }) => { + return ( +
+ +

+ + {item.scenario} +

+ {item.feature &&

{item.feature}

} + +
+
+
Status
+
{statusName(item.status)}
+
+
+
Duration
+
{formatExecutionDuration(new Date(item.start), new Date(item.end))}
+
+
+
Worker
+
{item.groupLabel}
+
+
+ +
+ ) +} diff --git a/src/components/app/Timeline.module.scss b/src/components/app/Timeline.module.scss index a8e4f64c..440fd080 100644 --- a/src/components/app/Timeline.module.scss +++ b/src/components/app/Timeline.module.scss @@ -1,5 +1,6 @@ @use '../../styles/statuses'; @use '../../styles/theming'; +@use 'sass:color'; .container { border: 1px solid theming.$panelAccentColor; @@ -73,4 +74,64 @@ dd { margin: 0; } +} + + + + + + +// NEW NEW NEW + +.timelineWrapper { + display: flex; + flex-direction: column; + gap: 1px; + background-color: theming.$panelAccentColor; + padding: 1px; +} + +.timelineRow { + width: 100%; + display: grid; + grid-template-columns: 1fr minmax(0, 4fr); + overscroll-behavior: none; + gap: 1px; +} + +.cell { + height: 2em; + background-color: theming.$panelBackgroundColor; + +} + +.workerCell { + display: flex; + align-items: center; + padding-left: 3px; +} + + +.workerRow { + display: flex; + overflow: hidden; + align-items: center; +} + +.timelineBar { + height: 80%; + border-radius: 0.333em; + border: none; +} + +@each $name, $color in statuses.$statusColors { + .timelineBar[data-status='#{$name}'] { + background-color: color.adjust($color, $alpha: -0.666); + border-color: $color; + } + + .timelineBar[data-status='#{$name}']:hover { + outline: 1px solid $color; + } + } \ No newline at end of file diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index 9c501bbe..c523f1a8 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -1,6 +1,4 @@ -import { type FC, useEffect, useRef, useState } from 'react' -import { DataSet } from 'vis-data' -import { Timeline as VisTimeline } from 'vis-timeline' +import { type FC, useEffect, useRef, useState, WheelEventHandler } from 'react' import { formatExecutionDuration } from '../../formatExecutionDuration.js' import { type TimelineItem, useTimelineData } from '../../hooks/useTimelineData.js' import { StatusIcon } from '../gherkin/StatusIcon.js' @@ -8,96 +6,99 @@ import statusName from '../gherkin/statusName.js' import { Tags } from '../gherkin/Tags.js' import { TestCaseOutcome } from '../results/index.js' import styles from './Timeline.module.scss' - -import 'vis-timeline/styles/vis-timeline-graph2d.css' import { faXmark } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +// import {TooltipTrigger} from 'react-aria-components'; +import { -type DataSetGroup = { - id: string - content: string -} + TooltipTrigger, + Tooltip, + Button, + type TooltipProps, + type TooltipTriggerComponentProps +} from 'react-aria-components'; -type DataSetItem = { - id: string - content: string - group: string - start: Date - end: Date - status: string - className: string -} -export const Timeline: FC = () => { - const { groups, items, fullStart, fullEnd, filtered } = useTimelineData() - const [selectedId, setSelectedId] = useState() - const containerRef = useRef(null) +export const Timeline: FC = () => { + const { groups, items, fullStart, fullEnd, filtered } = useTimelineData(); + const axisRef = useRef(null); + const [selectedId, setSelectedId] = useState(undefined); + const [axisUnit, setAxisUnit] = useState(100); + const [pxPerMs, setPxPerMs] = useState(10); + + // const pxPerMs = 10; + useEffect(() => { - if (!containerRef.current) { - return + const element = axisRef.current; + if (!element) { + return; } - - const dataSetGroups = new DataSet() - groups.forEach((g) => { - dataSetGroups.add({ id: g.id, content: g.label }) - }) - - const dataSetItems = new DataSet() - items.forEach((i) => { - dataSetItems.add({ - id: i.id, - content: i.scenario, - group: i.groupId, - start: new Date(i.start), - end: new Date(i.end), - status: i.status, - className: styles.visItem, - }) - }) - - const timeline = new VisTimeline(containerRef.current, dataSetItems, dataSetGroups, { - stack: false, - zoomable: true, - moveable: true, - selectable: true, - editable: false, - showCurrentTime: false, - orientation: 'top', - min: fullStart, - max: fullEnd, - start: fullStart, - end: fullEnd, - dataAttributes: ['status'], - }) - - timeline.on('select', (props: { items: string[] }) => { - setSelectedId(props.items[0] ?? undefined) - }) - - return () => { - timeline.destroy() + const handleAxisZoom = (e: WheelEvent) => { + e.preventDefault(); + const zoomDirection = e.deltaY < 0 ? 1: -1; + const zoomFactor = 1.1; + if(zoomDirection === -1) { + setAxisUnit(prev => prev * zoomFactor); + setPxPerMs(prev => prev / zoomFactor); + } else { + setAxisUnit(prev => prev / zoomFactor); + setPxPerMs(prev => prev * zoomFactor); + } } - }, [fullStart, fullEnd, groups, items]) + element.addEventListener('wheel', handleAxisZoom, { passive: false }); - if (items.length === 0) { - return filtered ? ( -

No scenarios match your query and/or filters.

- ) : ( -

No scenarios were executed.

- ) - } + return () => { + element.removeEventListener('wheel', handleAxisZoom); + }; + }, []); - const selectedItem = items.find((item) => item.id === selectedId) + const selectedItem = items.find((item) => item.id === selectedId); return ( -
-
- {selectedItem && ( + <> +
+ {/* Header */} + +
+
+
Axis Unit: {axisUnit}ms
+
+ + { + groups.map((grp) => { + return
+ +
+ {grp.label} +
+
+ {items.filter((i) => i.groupId === grp.id).map((item) => { + const width = (item.end - item.start) * pxPerMs; + + return + + + EDIT + + + + })} +
+ +
+ }) + } + + + +
+ {selectedItem && ( setSelectedId(undefined)} /> )} -
- ) + + ); + } const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item, onClose }) => { diff --git a/src/hooks/useFilteredTestCases.spec.tsx b/src/hooks/useFilteredTestCases.spec.tsx index 5ea17fc8..2c90e9a0 100644 --- a/src/hooks/useFilteredTestCases.spec.tsx +++ b/src/hooks/useFilteredTestCases.spec.tsx @@ -10,6 +10,7 @@ import rules from '../../acceptance/rules/rules.js' import { EnvelopesProvider } from '../components/app/EnvelopesProvider.js' import { InMemorySearchProvider } from '../components/app/InMemorySearchProvider.js' import { useFilteredTestCases } from './useFilteredTestCases.js' +import parallel from '../../acceptance/parallel/parallel.js' interface ProviderProps { envelopes: Parameters[0]['envelopes'] @@ -39,8 +40,9 @@ function renderAndExtractPickleNames({ describe('useFilteredTestCases', () => { describe('with no filters', () => { it('returns a test case for every finished scenario', async () => { - const { result } = renderAndExtractPickleNames({ envelopes: hooksConditional }) + // const { result } = renderAndExtractPickleNames({ envelopes: hooksConditional }) + const { result } = renderAndExtractPickleNames({ envelopes: parallel }) await waitFor(() => expect(result.current).to.have.members([ 'A failure in the before hook and a skipped step', diff --git a/src/hooks/useFilteredTestCases.ts b/src/hooks/useFilteredTestCases.ts index d80add38..f385d4b8 100644 --- a/src/hooks/useFilteredTestCases.ts +++ b/src/hooks/useFilteredTestCases.ts @@ -24,6 +24,7 @@ export function useFilteredTestCases(): ReadonlyArray>>([]) diff --git a/src/hooks/useTimelineData.ts b/src/hooks/useTimelineData.ts index 16ef807e..a135c5c9 100644 --- a/src/hooks/useTimelineData.ts +++ b/src/hooks/useTimelineData.ts @@ -8,6 +8,7 @@ import { useMemo } from 'react' import { useQueries } from './useQueries.js' import { useSearch } from './useSearch.js' +import { useFilteredTestCases } from './useFilteredTestCases.js' export interface TimelineItem { readonly id: string @@ -41,25 +42,27 @@ export function useTimelineData(): TimelineData { const { cucumberQuery } = useQueries() const { hideStatuses, tagExpression, searchTerm, unchanged } = useSearch() + const finishedTestCases = useFilteredTestCases(); return useMemo(() => { const items: TimelineItem[] = [] const groupIds = new Set() - const normalizedSearchTerm = searchTerm?.trim().toLowerCase() + // const normalizedSearchTerm = searchTerm?.trim().toLowerCase() let fullStart: number | undefined let fullEnd: number | undefined - for (const testCaseFinished of cucumberQuery.findAllTestCaseFinished()) { - const testCaseStarted = cucumberQuery.findTestCaseStartedBy(testCaseFinished) + // for (const testCaseFinished of cucumberQuery.findAllTestCaseFinished()) { + for (const testCaseFinished of finishedTestCases) { + const testCaseStarted = cucumberQuery.findTestCaseStartedBy(testCaseFinished.testCaseEvent) if (!testCaseStarted) { continue } - const pickle = cucumberQuery.findPickleBy(testCaseStarted) + const pickle = testCaseFinished.pickle; if (!pickle) { continue } const itemStart = TimeConversion.timestampToMillisecondsSinceEpoch(testCaseStarted.timestamp) - const itemEnd = TimeConversion.timestampToMillisecondsSinceEpoch(testCaseFinished.timestamp) + const itemEnd = TimeConversion.timestampToMillisecondsSinceEpoch(testCaseFinished.testCaseEvent.timestamp) if (fullStart === undefined || itemStart < fullStart) { fullStart = itemStart @@ -70,29 +73,29 @@ export function useTimelineData(): TimelineData { // A test case with no step results at all is considered passed by definition const status = - cucumberQuery.findMostSevereTestStepResultBy(testCaseFinished)?.status ?? + cucumberQuery.findMostSevereTestStepResultBy(testCaseFinished.testCaseEvent)?.status ?? TestStepResultStatus.PASSED - if (hideStatuses.includes(status)) { - continue - } + // if (hideStatuses.includes(status)) { + // continue + // } - if (tagExpression) { - const tagNames = pickle.tags.map((tag) => tag.name) - if (!tagExpression.evaluate(tagNames)) { - continue - } - } + // if (tagExpression) { + // const tagNames = pickle.tags.map((tag) => tag.name) + // if (!tagExpression.evaluate(tagNames)) { + // continue + // } + // } - const feature = cucumberQuery.findLineageBy(testCaseStarted)?.feature?.name ?? '' + const feature = testCaseFinished.lineage.feature?.name ?? '' const scenario = pickle.name - if ( - normalizedSearchTerm && - !`${feature} ${scenario}`.toLowerCase().includes(normalizedSearchTerm) - ) { - continue - } + // if ( + // normalizedSearchTerm && + // !`${feature} ${scenario}`.toLowerCase().includes(normalizedSearchTerm) + // ) { + // continue + // } const groupId = testCaseStarted.workerId ?? UNASSIGNED_GROUP_ID groupIds.add(groupId) @@ -118,7 +121,7 @@ export function useTimelineData(): TimelineData { .map((id) => ({ id, label: describeGroup(id) })) return { groups, items, fullStart, fullEnd, filtered: !unchanged } - }, [cucumberQuery, hideStatuses, tagExpression, searchTerm, unchanged]) + }, [cucumberQuery, unchanged, finishedTestCases]) } function describeGroup(id: string): string { From 3e41b836944920b58c77b873dfb4e227ff130ead Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Sun, 26 Jul 2026 13:47:29 +0500 Subject: [PATCH 11/26] Implemented Basic Layout of Timeline --- src/components/app/CustomTimeline.tsx | 132 ------------------------ src/components/app/Timeline.module.scss | 80 +++++++++----- src/components/app/Timeline.tsx | 50 ++++----- 3 files changed, 81 insertions(+), 181 deletions(-) delete mode 100644 src/components/app/CustomTimeline.tsx diff --git a/src/components/app/CustomTimeline.tsx b/src/components/app/CustomTimeline.tsx deleted file mode 100644 index 9c501bbe..00000000 --- a/src/components/app/CustomTimeline.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { type FC, useEffect, useRef, useState } from 'react' -import { DataSet } from 'vis-data' -import { Timeline as VisTimeline } from 'vis-timeline' -import { formatExecutionDuration } from '../../formatExecutionDuration.js' -import { type TimelineItem, useTimelineData } from '../../hooks/useTimelineData.js' -import { StatusIcon } from '../gherkin/StatusIcon.js' -import statusName from '../gherkin/statusName.js' -import { Tags } from '../gherkin/Tags.js' -import { TestCaseOutcome } from '../results/index.js' -import styles from './Timeline.module.scss' - -import 'vis-timeline/styles/vis-timeline-graph2d.css' -import { faXmark } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' - -type DataSetGroup = { - id: string - content: string -} - -type DataSetItem = { - id: string - content: string - group: string - start: Date - end: Date - status: string - className: string -} -export const Timeline: FC = () => { - const { groups, items, fullStart, fullEnd, filtered } = useTimelineData() - const [selectedId, setSelectedId] = useState() - - const containerRef = useRef(null) - - useEffect(() => { - if (!containerRef.current) { - return - } - - const dataSetGroups = new DataSet() - groups.forEach((g) => { - dataSetGroups.add({ id: g.id, content: g.label }) - }) - - const dataSetItems = new DataSet() - items.forEach((i) => { - dataSetItems.add({ - id: i.id, - content: i.scenario, - group: i.groupId, - start: new Date(i.start), - end: new Date(i.end), - status: i.status, - className: styles.visItem, - }) - }) - - const timeline = new VisTimeline(containerRef.current, dataSetItems, dataSetGroups, { - stack: false, - zoomable: true, - moveable: true, - selectable: true, - editable: false, - showCurrentTime: false, - orientation: 'top', - min: fullStart, - max: fullEnd, - start: fullStart, - end: fullEnd, - dataAttributes: ['status'], - }) - - timeline.on('select', (props: { items: string[] }) => { - setSelectedId(props.items[0] ?? undefined) - }) - - return () => { - timeline.destroy() - } - }, [fullStart, fullEnd, groups, items]) - - if (items.length === 0) { - return filtered ? ( -

No scenarios match your query and/or filters.

- ) : ( -

No scenarios were executed.

- ) - } - - const selectedItem = items.find((item) => item.id === selectedId) - - return ( -
-
- {selectedItem && ( - setSelectedId(undefined)} /> - )} -
- ) -} - -const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item, onClose }) => { - return ( -
- -

- - {item.scenario} -

- {item.feature &&

{item.feature}

} - -
-
-
Status
-
{statusName(item.status)}
-
-
-
Duration
-
{formatExecutionDuration(new Date(item.start), new Date(item.end))}
-
-
-
Worker
-
{item.groupLabel}
-
-
- -
- ) -} diff --git a/src/components/app/Timeline.module.scss b/src/components/app/Timeline.module.scss index 440fd080..9c7b7014 100644 --- a/src/components/app/Timeline.module.scss +++ b/src/components/app/Timeline.module.scss @@ -2,26 +2,6 @@ @use '../../styles/theming'; @use 'sass:color'; -.container { - border: 1px solid theming.$panelAccentColor; -} - -@each $name, $color in statuses.$statusColors { - .visItem[data-status='#{$name}'] { - background-color: $color; - border-color: $color; - } - -.visItem[data-status='#{$name}']:global(.vis-selected) { - background-color: $color; - border-color: $color; - - outline: 2px solid theming.$panelTextColor; - outline-offset: 1px; - z-index: 2; - } -} - .detail { position: relative; padding: 1em; @@ -86,8 +66,8 @@ .timelineWrapper { display: flex; flex-direction: column; - gap: 1px; - background-color: theming.$panelAccentColor; + // gap: 1px; + // background-color: theming.$panelAccentColor; padding: 1px; } @@ -97,18 +77,30 @@ grid-template-columns: 1fr minmax(0, 4fr); overscroll-behavior: none; gap: 1px; + border: 1px solid theming.$panelAccentColor; + border-top: none; +} + +.timelineRow:first-child { + border-top: 1px solid theming.$panelAccentColor; } .cell { height: 2em; - background-color: theming.$panelBackgroundColor; + // background-color: theming.$panelBackgroundColor; + background-color: white; + +} +.cell:first-child { + border-right: 1px solid theming.$panelAccentColor; } .workerCell { display: flex; align-items: center; - padding-left: 3px; + padding-left: 0.5em; + // font-family: inherit; } @@ -128,10 +120,46 @@ .timelineBar[data-status='#{$name}'] { background-color: color.adjust($color, $alpha: -0.666); border-color: $color; - } + - .timelineBar[data-status='#{$name}']:hover { + &:hover { outline: 1px solid $color; } + &.selected { + outline: 1px solid $color + } + +} + +} + +.icon { + padding-top: 0.35em; + padding-left: 0.5em; + padding-right: 0.5em; +} + +// Tooltip card +:global(.react-aria-Tooltip) { + background-color: theming.$panelBackgroundColor; + color: theming.$panelTextColor; + border: 1px solid theming.$panelAccentColor; + border-radius: 0.25em; + padding: 0.4em 0.6em; + font-size: inherit; + font-family: inherit; + display: flex; + align-items: center; + gap: 0.4em; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); + max-width: 20em; + margin-bottom: 0.7em; +} + +// Arrow styling +:global(.react-aria-Tooltip) svg { + fill: theming.$exampleNumberColor; + stroke: theming.$panelAccentColor; + stroke-width: 1px; } \ No newline at end of file diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index c523f1a8..c579e8d7 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -1,4 +1,4 @@ -import { type FC, useEffect, useRef, useState, WheelEventHandler } from 'react' +import { type FC, useEffect, useRef, useState } from 'react' import { formatExecutionDuration } from '../../formatExecutionDuration.js' import { type TimelineItem, useTimelineData } from '../../hooks/useTimelineData.js' import { StatusIcon } from '../gherkin/StatusIcon.js' @@ -8,14 +8,11 @@ import { TestCaseOutcome } from '../results/index.js' import styles from './Timeline.module.scss' import { faXmark } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -// import {TooltipTrigger} from 'react-aria-components'; import { - TooltipTrigger, Tooltip, Button, - type TooltipProps, - type TooltipTriggerComponentProps + OverlayArrow, } from 'react-aria-components'; @@ -27,8 +24,6 @@ export const Timeline: FC = () => { const [axisUnit, setAxisUnit] = useState(100); const [pxPerMs, setPxPerMs] = useState(10); - // const pxPerMs = 10; - useEffect(() => { const element = axisRef.current; if (!element) { @@ -58,40 +53,29 @@ export const Timeline: FC = () => { return ( <>
- {/* Header */}
-
Axis Unit: {axisUnit}ms
+
+ { groups.map((grp) => { return
- +
{grp.label}
+
- {items.filter((i) => i.groupId === grp.id).map((item) => { - const width = (item.end - item.start) * pxPerMs; + {items.filter((i) => i.groupId === grp.id).map((item) => )} - return - - - EDIT - - - - })}
-
}) } - -
{selectedItem && ( setSelectedId(undefined)} /> @@ -101,6 +85,26 @@ export const Timeline: FC = () => { } +const TimelineBar: FC<{item: TimelineItem, width: number, selectedId: string | undefined, setSelectedId: (id: string) => void}> = ({item, width, selectedId, setSelectedId}) => { + return + + + + + Tooltip Arrow + + + + + + + {item.scenario} + + +} + + + const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item, onClose }) => { return (
From e72c7c1c0404dadbb888502e31a16f40b4378e58 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Sat, 1 Aug 2026 22:13:19 +0500 Subject: [PATCH 12/26] Separated Timeline Axis Component --- src/components/app/Timeline.module.scss | 6 + src/components/app/Timeline.tsx | 241 ++++++++++++++++-------- src/hooks/useFilteredTestCases.spec.tsx | 2 +- src/hooks/useFilteredTestCases.ts | 2 +- src/hooks/useTimelineData.ts | 11 +- 5 files changed, 177 insertions(+), 85 deletions(-) diff --git a/src/components/app/Timeline.module.scss b/src/components/app/Timeline.module.scss index 9c7b7014..835547be 100644 --- a/src/components/app/Timeline.module.scss +++ b/src/components/app/Timeline.module.scss @@ -108,12 +108,18 @@ display: flex; overflow: hidden; align-items: center; + flex-shrink: 0; } .timelineBar { height: 80%; border-radius: 0.333em; border: none; + + padding: 0; + min-width: 0; // Overrides flexbox min-width: auto + box-sizing: border-box; // Ensures borders/padding don't add to the width + flex: none; // Prevents flexbox from growing or shrinking it } @each $name, $color in statuses.$statusColors { diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index c579e8d7..5e368b72 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -1,4 +1,7 @@ +import { faXmark } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { type FC, useEffect, useRef, useState } from 'react' +import { Button, OverlayArrow, Tooltip, TooltipTrigger } from 'react-aria-components' import { formatExecutionDuration } from '../../formatExecutionDuration.js' import { type TimelineItem, useTimelineData } from '../../hooks/useTimelineData.js' import { StatusIcon } from '../gherkin/StatusIcon.js' @@ -6,105 +9,187 @@ import statusName from '../gherkin/statusName.js' import { Tags } from '../gherkin/Tags.js' import { TestCaseOutcome } from '../results/index.js' import styles from './Timeline.module.scss' -import { faXmark } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { - TooltipTrigger, - Tooltip, - Button, - OverlayArrow, -} from 'react-aria-components'; +type unit = { + label: string + magnitude: number +} + +const axisUnits: unit[] = [ + { label: '1 ms', magnitude: 1 }, + { label: '10 ms', magnitude: 10 }, + { label: '50 ms', magnitude: 50 }, + { label: '100 ms', magnitude: 100 }, + { label: '500 ms', magnitude: 500 }, + { label: '1 s', magnitude: 1 * 1000 }, + { label: '10 s', magnitude: 10 * 1000 }, + { label: '30 s', magnitude: 30 * 1000 }, + { label: '1 min', magnitude: 1 * 60 * 1000 }, + { label: '10 min', magnitude: 10 * 60 * 1000 }, + { label: '30 min', magnitude: 30 * 60 * 1000 }, + { label: '1 hr', magnitude: 1 * 60 * 60 * 1000 }, +] export const Timeline: FC = () => { - const { groups, items, fullStart, fullEnd, filtered } = useTimelineData(); - const axisRef = useRef(null); - const [selectedId, setSelectedId] = useState(undefined); + const { groups, items, fullStart, fullEnd, filtered } = useTimelineData() + const [selectedId, setSelectedId] = useState(undefined) + const [axisUnitIndex, setAxisUnitIndex] = useState(0) + const [rowWidthPx, setRowWidthPx] = useState(0) + const pxPerUnit = 20 + + const selectedItem = items.find((item) => item.id === selectedId) + + return ( + <> +
+ + + {groups.map((grp) => { + return ( +
+
{grp.label}
+ +
+ {items + .filter((i) => i.groupId === grp.id) + .map((item) => { + if (rowWidthPx === 0) { + return null + } + + const magnitude = axisUnits[axisUnitIndex].magnitude + const duration = item.end - item.start + + const widthInUnits = duration / magnitude + const leftOffsetInUnits = (item.start - (fullStart ?? 0)) / magnitude + + const widthInPx = widthInUnits * pxPerUnit + const leftOffsetInPx = leftOffsetInUnits * pxPerUnit + + const widthPercent = (widthInPx / rowWidthPx) * 100 + const leftPercent = (leftOffsetInPx / rowWidthPx) * 100 + + // console.log(duration, widthPercent); + return ( + + ) + })} +
+
+ ) + })} +
+ {selectedItem && ( + setSelectedId(undefined)} /> + )} + + ) +} + +const TimelineAxis: FC<{ + axisUnitIndex: number + setAxisUnitIndex: React.Dispatch> + setRowWidthPx: React.Dispatch> +}> = ({ axisUnitIndex, setAxisUnitIndex, setRowWidthPx }) => { + const axisRef = useRef(null) - const [axisUnit, setAxisUnit] = useState(100); - const [pxPerMs, setPxPerMs] = useState(10); - useEffect(() => { - const element = axisRef.current; + const element = axisRef.current if (!element) { - return; - } - const handleAxisZoom = (e: WheelEvent) => { - e.preventDefault(); - const zoomDirection = e.deltaY < 0 ? 1: -1; - const zoomFactor = 1.1; - if(zoomDirection === -1) { - setAxisUnit(prev => prev * zoomFactor); - setPxPerMs(prev => prev / zoomFactor); - } else { - setAxisUnit(prev => prev / zoomFactor); - setPxPerMs(prev => prev * zoomFactor); - } + return } - element.addEventListener('wheel', handleAxisZoom, { passive: false }); - return () => { - element.removeEventListener('wheel', handleAxisZoom); - }; - }, []); + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + setRowWidthPx(entry.contentRect.width) + } + }) - const selectedItem = items.find((item) => item.id === selectedId); + observer.observe(element) + return () => observer.disconnect() + }, [setRowWidthPx]) - return ( - <> -
+ useEffect(() => { + console.log('USE EFF CHALA') + const element = axisRef.current + if (!element) { + return + } -
-
-
-
+ console.log('ADDED ZOOM HANDLER') + const handleAxisZoom = (e: WheelEvent) => { + console.log('HANDLING ZOOM') + const zoomDirection = e.deltaY < 0 ? -1 : 1 + e.preventDefault() - { - groups.map((grp) => { - return
- -
- {grp.label} -
+ if (zoomDirection === 1) { + setAxisUnitIndex((prev) => Math.min(axisUnits.length - 1, prev + 1)) + } else { + setAxisUnitIndex((prev) => Math.max(0, prev - 1)) + } + } -
- {items.filter((i) => i.groupId === grp.id).map((item) => )} + element.addEventListener('wheel', handleAxisZoom, { passive: false }) -
-
- }) - } + return () => { + element.removeEventListener('wheel', handleAxisZoom) + } + }) + return ( +
+
+
+ {axisUnits[axisUnitIndex].label} +
- {selectedItem && ( - setSelectedId(undefined)} /> - )} - - ); - + ) } -const TimelineBar: FC<{item: TimelineItem, width: number, selectedId: string | undefined, setSelectedId: (id: string) => void}> = ({item, width, selectedId, setSelectedId}) => { - return - - - - - Tooltip Arrow - - - - - - - {item.scenario} - - +const TimelineBar: FC<{ + item: TimelineItem + width: number + left: number + selectedId: string | undefined + setSelectedId: (id: string) => void +}> = ({ item, width, left, selectedId, setSelectedId }) => { + return ( + + + + + + Tooltip Arrow + + + + + + + {item.scenario} + + + ) } - - const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item, onClose }) => { return (
diff --git a/src/hooks/useFilteredTestCases.spec.tsx b/src/hooks/useFilteredTestCases.spec.tsx index 2c90e9a0..ce4fb2cb 100644 --- a/src/hooks/useFilteredTestCases.spec.tsx +++ b/src/hooks/useFilteredTestCases.spec.tsx @@ -5,12 +5,12 @@ import { expect } from 'chai' import attachments from '../../acceptance/attachments/attachments.js' import backgrounds from '../../acceptance/backgrounds/backgrounds.js' import hooksConditional from '../../acceptance/hooks-conditional/hooks-conditional.js' +import parallel from '../../acceptance/parallel/parallel.js' import retry from '../../acceptance/retry/retry.js' import rules from '../../acceptance/rules/rules.js' import { EnvelopesProvider } from '../components/app/EnvelopesProvider.js' import { InMemorySearchProvider } from '../components/app/InMemorySearchProvider.js' import { useFilteredTestCases } from './useFilteredTestCases.js' -import parallel from '../../acceptance/parallel/parallel.js' interface ProviderProps { envelopes: Parameters[0]['envelopes'] diff --git a/src/hooks/useFilteredTestCases.ts b/src/hooks/useFilteredTestCases.ts index f385d4b8..54c8ffa6 100644 --- a/src/hooks/useFilteredTestCases.ts +++ b/src/hooks/useFilteredTestCases.ts @@ -24,7 +24,7 @@ export function useFilteredTestCases(): ReadonlyArray>>([]) diff --git a/src/hooks/useTimelineData.ts b/src/hooks/useTimelineData.ts index a135c5c9..271e0f7a 100644 --- a/src/hooks/useTimelineData.ts +++ b/src/hooks/useTimelineData.ts @@ -5,10 +5,9 @@ import { TimeConversion, } from '@cucumber/messages' import { useMemo } from 'react' - +import { useFilteredTestCases } from './useFilteredTestCases.js' import { useQueries } from './useQueries.js' import { useSearch } from './useSearch.js' -import { useFilteredTestCases } from './useFilteredTestCases.js' export interface TimelineItem { readonly id: string @@ -42,7 +41,7 @@ export function useTimelineData(): TimelineData { const { cucumberQuery } = useQueries() const { hideStatuses, tagExpression, searchTerm, unchanged } = useSearch() - const finishedTestCases = useFilteredTestCases(); + const finishedTestCases = useFilteredTestCases() return useMemo(() => { const items: TimelineItem[] = [] const groupIds = new Set() @@ -56,13 +55,15 @@ export function useTimelineData(): TimelineData { if (!testCaseStarted) { continue } - const pickle = testCaseFinished.pickle; + const pickle = testCaseFinished.pickle if (!pickle) { continue } const itemStart = TimeConversion.timestampToMillisecondsSinceEpoch(testCaseStarted.timestamp) - const itemEnd = TimeConversion.timestampToMillisecondsSinceEpoch(testCaseFinished.testCaseEvent.timestamp) + const itemEnd = TimeConversion.timestampToMillisecondsSinceEpoch( + testCaseFinished.testCaseEvent.timestamp + ) if (fullStart === undefined || itemStart < fullStart) { fullStart = itemStart From 4fea46742d1d1e74775522979fc67c91c9c10f42 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Mon, 3 Aug 2026 19:14:17 +0500 Subject: [PATCH 13/26] Added Panning --- src/components/app/Timeline.module.scss | 6 +++ src/components/app/Timeline.spec.tsx | 27 +++++++++- src/components/app/Timeline.tsx | 65 +++++++++++++++++++------ src/hooks/useTimelineData.ts | 10 ++-- 4 files changed, 87 insertions(+), 21 deletions(-) diff --git a/src/components/app/Timeline.module.scss b/src/components/app/Timeline.module.scss index 835547be..c6bb5d03 100644 --- a/src/components/app/Timeline.module.scss +++ b/src/components/app/Timeline.module.scss @@ -103,12 +103,17 @@ // font-family: inherit; } +.axis { + border:none; +} + .workerRow { display: flex; overflow: hidden; align-items: center; flex-shrink: 0; + // position: absolute; } .timelineBar { @@ -120,6 +125,7 @@ min-width: 0; // Overrides flexbox min-width: auto box-sizing: border-box; // Ensures borders/padding don't add to the width flex: none; // Prevents flexbox from growing or shrinking it + // position: relative; } @each $name, $color in statuses.$statusColors { diff --git a/src/components/app/Timeline.spec.tsx b/src/components/app/Timeline.spec.tsx index dfdeeb23..e1186a9f 100644 --- a/src/components/app/Timeline.spec.tsx +++ b/src/components/app/Timeline.spec.tsx @@ -1,5 +1,5 @@ import { type Envelope, type TestCaseStarted, TestStepResultStatus } from '@cucumber/messages' -import { render, screen, within } from '@testing-library/react' +import { fireEvent, render, screen, within } from '@testing-library/react' import { userEvent } from '@testing-library/user-event' import { expect } from 'chai' @@ -96,6 +96,31 @@ describe('', () => { expect(screen.getByText('Worker 1')).to.be.visible }) + it('should render an axis ruler that updates when zooming and panning', () => { + render( + + {}}> + + + + ) + + const axis = screen.getByRole('button', { name: 'Timeline axis scale 10 ms' }) + + expect(within(axis).getByText('Scale')).to.be.visible + expect(within(axis).getByText('0 seconds')).to.be.visible + + fireEvent.wheel(axis, { deltaY: 1 }) + + expect(screen.getByRole('button', { name: 'Timeline axis scale 50 ms' })).to.be.visible + + fireEvent.mouseDown(axis) + fireEvent.mouseMove(axis, { movementX: -20 }) + fireEvent.mouseUp(axis) + + expect(within(axis).getByText('0.01 seconds')).to.be.visible + }) + it('should show scenario details when a bar is selected, and hide them again on close', async () => { render( diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index 5e368b72..ff1ea924 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -33,8 +33,9 @@ const axisUnits: unit[] = [ export const Timeline: FC = () => { const { groups, items, fullStart, fullEnd, filtered } = useTimelineData() const [selectedId, setSelectedId] = useState(undefined) - const [axisUnitIndex, setAxisUnitIndex] = useState(0) - const [rowWidthPx, setRowWidthPx] = useState(0) + const [axisUnitIndex, setAxisUnitIndex] = useState(1); + const [axisStart, setAxisStart] = useState(undefined); + const [rowWidthPx, setRowWidthPx] = useState(0); const pxPerUnit = 20 const selectedItem = items.find((item) => item.id === selectedId) @@ -46,13 +47,19 @@ export const Timeline: FC = () => { axisUnitIndex={axisUnitIndex} setAxisUnitIndex={setAxisUnitIndex} setRowWidthPx={setRowWidthPx} + axisStart={axisStart ?? 0} + setAxisStart={setAxisStart} + fullStart={fullStart} + fullEnd={fullEnd} > {groups.map((grp) => { + let pre = axisStart ?? 0; return (
{grp.label}
+
{items .filter((i) => i.groupId === grp.id) @@ -65,7 +72,7 @@ export const Timeline: FC = () => { const duration = item.end - item.start const widthInUnits = duration / magnitude - const leftOffsetInUnits = (item.start - (fullStart ?? 0)) / magnitude + const leftOffsetInUnits = (item.start - (pre)) / magnitude const widthInPx = widthInUnits * pxPerUnit const leftOffsetInPx = leftOffsetInUnits * pxPerUnit @@ -73,7 +80,8 @@ export const Timeline: FC = () => { const widthPercent = (widthInPx / rowWidthPx) * 100 const leftPercent = (leftOffsetInPx / rowWidthPx) * 100 - // console.log(duration, widthPercent); + pre = item.end; + return ( > setRowWidthPx: React.Dispatch> -}> = ({ axisUnitIndex, setAxisUnitIndex, setRowWidthPx }) => { - const axisRef = useRef(null) + fullStart: number + fullEnd: number + axisStart: number + setAxisStart: React.Dispatch> +}> = ({ axisUnitIndex, setAxisUnitIndex, setRowWidthPx, fullStart, fullEnd, axisStart, setAxisStart }) => { + const axisRef = useRef(null) + const isDragging = useRef(false); + // Setting Axis Start useEffect(() => { + + setAxisStart(fullStart); + const element = axisRef.current if (!element) { return @@ -118,19 +135,16 @@ const TimelineAxis: FC<{ observer.observe(element) return () => observer.disconnect() - }, [setRowWidthPx]) + }, [setRowWidthPx, setAxisStart, fullStart]) + // Regisetering Handle Zoom Callback useEffect(() => { - console.log('USE EFF CHALA') const element = axisRef.current if (!element) { return } - console.log('ADDED ZOOM HANDLER') - const handleAxisZoom = (e: WheelEvent) => { - console.log('HANDLING ZOOM') const zoomDirection = e.deltaY < 0 ? -1 : 1 e.preventDefault() @@ -148,12 +162,25 @@ const TimelineAxis: FC<{ } }) + + const handleAxisPanning = (deltaX: number) => { + if(!isDragging.current) { + return; + } + + if(deltaX < 0) { + setAxisStart(prev => Math.min(fullEnd, prev + axisUnits[axisUnitIndex].magnitude)); + } else { + setAxisStart(prev => Math.max(fullStart, prev - axisUnits[axisUnitIndex].magnitude)); + } + } + return (
-
- {axisUnits[axisUnitIndex].label} -
+
) } @@ -170,7 +197,7 @@ const TimelineBar: FC<{ @@ -207,7 +234,15 @@ const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item,
Status
{statusName(item.status)}
+
+
Start
+
{item.start}
+
+
+
End
+
{item.end}
+
Duration
{formatExecutionDuration(new Date(item.start), new Date(item.end))}
diff --git a/src/hooks/useTimelineData.ts b/src/hooks/useTimelineData.ts index 271e0f7a..c5b07fa3 100644 --- a/src/hooks/useTimelineData.ts +++ b/src/hooks/useTimelineData.ts @@ -30,8 +30,8 @@ export interface TimelineGroup { export interface TimelineData { readonly groups: readonly TimelineGroup[] readonly items: readonly TimelineItem[] - readonly fullStart: number | undefined - readonly fullEnd: number | undefined + readonly fullStart: number + readonly fullEnd: number readonly filtered: boolean } @@ -46,8 +46,8 @@ export function useTimelineData(): TimelineData { const items: TimelineItem[] = [] const groupIds = new Set() // const normalizedSearchTerm = searchTerm?.trim().toLowerCase() - let fullStart: number | undefined - let fullEnd: number | undefined + let fullStart: number = Number.MAX_SAFE_INTEGER; + let fullEnd: number = Number.MIN_SAFE_INTEGER; // for (const testCaseFinished of cucumberQuery.findAllTestCaseFinished()) { for (const testCaseFinished of finishedTestCases) { @@ -65,7 +65,7 @@ export function useTimelineData(): TimelineData { testCaseFinished.testCaseEvent.timestamp ) - if (fullStart === undefined || itemStart < fullStart) { + if (itemStart < fullStart) { fullStart = itemStart } if (fullEnd === undefined || itemEnd > fullEnd) { From 2fe59969a9ae5e5b5e51ac6e7e0ccbc1352ec388 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 4 Aug 2026 00:08:58 +0500 Subject: [PATCH 14/26] Fixed Panning and Zoom --- src/components/app/Timeline.module.scss | 7 +- src/components/app/Timeline.tsx | 138 ++++++++++++++---------- 2 files changed, 88 insertions(+), 57 deletions(-) diff --git a/src/components/app/Timeline.module.scss b/src/components/app/Timeline.module.scss index c6bb5d03..ab185a95 100644 --- a/src/components/app/Timeline.module.scss +++ b/src/components/app/Timeline.module.scss @@ -79,6 +79,7 @@ gap: 1px; border: 1px solid theming.$panelAccentColor; border-top: none; + // overflow: hidden; } .timelineRow:first-child { @@ -114,6 +115,9 @@ align-items: center; flex-shrink: 0; // position: absolute; + position: sticky; + // transform: translate3d(200px, 0px, 0px); + // border: 1px solid black; } .timelineBar { @@ -125,7 +129,8 @@ min-width: 0; // Overrides flexbox min-width: auto box-sizing: border-box; // Ensures borders/padding don't add to the width flex: none; // Prevents flexbox from growing or shrinking it - // position: relative; + position: absolute; + } @each $name, $color in statuses.$statusColors { diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index ff1ea924..1846aa17 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -9,6 +9,7 @@ import statusName from '../gherkin/statusName.js' import { Tags } from '../gherkin/Tags.js' import { TestCaseOutcome } from '../results/index.js' import styles from './Timeline.module.scss' +import { useDebouncedCallback } from 'use-debounce' type unit = { label: string @@ -30,31 +31,41 @@ const axisUnits: unit[] = [ { label: '1 hr', magnitude: 1 * 60 * 60 * 1000 }, ] +const pxPerUnit = 20 export const Timeline: FC = () => { - const { groups, items, fullStart, fullEnd, filtered } = useTimelineData() + const { groups, items, fullStart, fullEnd } = useTimelineData() const [selectedId, setSelectedId] = useState(undefined) - const [axisUnitIndex, setAxisUnitIndex] = useState(1); - const [axisStart, setAxisStart] = useState(undefined); + // const [axisUnitIndex, setAxisUnitIndex] = useState(0); const [rowWidthPx, setRowWidthPx] = useState(0); - const pxPerUnit = 20 + + const timelineWrapperRef = useRef(null) + const axisStartRef = useRef(fullStart) + const axisUnitRef = useRef(0) const selectedItem = items.find((item) => item.id === selectedId) + useEffect(() => { + if (timelineWrapperRef.current) { + timelineWrapperRef.current.style.setProperty('--magnitude', axisUnits[axisUnitRef.current].magnitude.toString()) + timelineWrapperRef.current.style.setProperty('--axis-start', axisStartRef.current.toString()) + } + }, []) + return ( <> -
+
{groups.map((grp) => { - let pre = axisStart ?? 0; return (
{grp.label}
@@ -67,27 +78,10 @@ export const Timeline: FC = () => { if (rowWidthPx === 0) { return null } - - const magnitude = axisUnits[axisUnitIndex].magnitude - const duration = item.end - item.start - - const widthInUnits = duration / magnitude - const leftOffsetInUnits = (item.start - (pre)) / magnitude - - const widthInPx = widthInUnits * pxPerUnit - const leftOffsetInPx = leftOffsetInUnits * pxPerUnit - - const widthPercent = (widthInPx / rowWidthPx) * 100 - const leftPercent = (leftOffsetInPx / rowWidthPx) * 100 - - pre = item.end; - return ( @@ -106,21 +100,21 @@ export const Timeline: FC = () => { } const TimelineAxis: FC<{ - axisUnitIndex: number - setAxisUnitIndex: React.Dispatch> setRowWidthPx: React.Dispatch> fullStart: number fullEnd: number - axisStart: number - setAxisStart: React.Dispatch> -}> = ({ axisUnitIndex, setAxisUnitIndex, setRowWidthPx, fullStart, fullEnd, axisStart, setAxisStart }) => { + axisStartRef: React.RefObject + timelineWrapperRef: React.RefObject + axisUnitRef: React.RefObject +}> = ({ setRowWidthPx, fullStart, fullEnd, axisStartRef, timelineWrapperRef, axisUnitRef }) => { const axisRef = useRef(null) const isDragging = useRef(false); // Setting Axis Start useEffect(() => { - setAxisStart(fullStart); + axisStartRef.current = fullStart; + axisUnitRef.current = 0; const element = axisRef.current if (!element) { @@ -135,51 +129,85 @@ const TimelineAxis: FC<{ observer.observe(element) return () => observer.disconnect() - }, [setRowWidthPx, setAxisStart, fullStart]) + }, [setRowWidthPx, axisStartRef, fullStart, axisUnitRef]) // Regisetering Handle Zoom Callback + const handleAxisZoom = useDebouncedCallback((deltaY: number) => { + if(!timelineWrapperRef?.current || !axisRef.current) { + return; + } + const zoomDirection = deltaY < 0 ? -1 : 1 + + let newAxisUnitIndex = 0; + if (zoomDirection === 1) { + newAxisUnitIndex = Math.min(axisUnits.length - 1, axisUnitRef.current + 1) + } else { + newAxisUnitIndex = Math.max(0, axisUnitRef.current - 1) + } + + axisUnitRef.current = newAxisUnitIndex; + + axisRef.current.textContent = `Axis Unit: ${axisUnits[newAxisUnitIndex].label}` + timelineWrapperRef.current.style.setProperty('--magnitude', axisUnits[newAxisUnitIndex].magnitude.toString()); + }, 100) + useEffect(() => { const element = axisRef.current if (!element) { return } - const handleAxisZoom = (e: WheelEvent) => { - const zoomDirection = e.deltaY < 0 ? -1 : 1 - e.preventDefault() - - if (zoomDirection === 1) { - setAxisUnitIndex((prev) => Math.min(axisUnits.length - 1, prev + 1)) - } else { - setAxisUnitIndex((prev) => Math.max(0, prev - 1)) - } + const onWheel = (event: WheelEvent) => { + event.preventDefault() + handleAxisZoom(event.deltaY) } - element.addEventListener('wheel', handleAxisZoom, { passive: false }) - + element.addEventListener('wheel', onWheel, { passive: false }) return () => { - element.removeEventListener('wheel', handleAxisZoom) + element.removeEventListener('wheel', onWheel) + handleAxisZoom.cancel() } - }) + }, [handleAxisZoom]) + // useEffect(() => { + // const element = axisRef.current + // if (!element) { + // return + // } + + + + // element.addEventListener('wheel', handleAxisZoom, { passive: false }) + + // // useDebounce(handleAxisZoom, 1000) + // return () => { + // element.removeEventListener('wheel', handleAxisZoom) + // } + // }) + + const handleAxisPanning = (deltaX: number) => { - if(!isDragging.current) { + if(!isDragging.current || !timelineWrapperRef?.current) { return; } + let newStart = 0; if(deltaX < 0) { - setAxisStart(prev => Math.min(fullEnd, prev + axisUnits[axisUnitIndex].magnitude)); + newStart = Math.min(fullEnd, axisStartRef.current + axisUnits[axisUnitRef.current].magnitude) } else { - setAxisStart(prev => Math.max(fullStart, prev - axisUnits[axisUnitIndex].magnitude)); + newStart = Math.max(fullStart, axisStartRef.current - axisUnits[axisUnitRef.current].magnitude) } + + axisStartRef.current = newStart; + timelineWrapperRef.current.style.setProperty('--axis-start', newStart.toString()); } return (
) @@ -187,19 +215,17 @@ const TimelineAxis: FC<{ const TimelineBar: FC<{ item: TimelineItem - width: number - left: number selectedId: string | undefined setSelectedId: (id: string) => void -}> = ({ item, width, left, selectedId, setSelectedId }) => { +}> = ({ item, selectedId, setSelectedId }) => { return ( From c860765bb7e7c3dac63445a510228b8ff25a1c92 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 4 Aug 2026 14:26:24 +0500 Subject: [PATCH 15/26] Fixed useRef issue --- src/components/app/Timeline.tsx | 27 ++++----------------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index 1846aa17..2c8e58ec 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -31,11 +31,11 @@ const axisUnits: unit[] = [ { label: '1 hr', magnitude: 1 * 60 * 60 * 1000 }, ] -const pxPerUnit = 20 +const pxPerUnit = 2 + export const Timeline: FC = () => { const { groups, items, fullStart, fullEnd } = useTimelineData() const [selectedId, setSelectedId] = useState(undefined) - // const [axisUnitIndex, setAxisUnitIndex] = useState(0); const [rowWidthPx, setRowWidthPx] = useState(0); const timelineWrapperRef = useRef(null) @@ -46,17 +46,16 @@ export const Timeline: FC = () => { useEffect(() => { if (timelineWrapperRef.current) { + console.log(`${fullStart} loaded`) timelineWrapperRef.current.style.setProperty('--magnitude', axisUnits[axisUnitRef.current].magnitude.toString()) timelineWrapperRef.current.style.setProperty('--axis-start', axisStartRef.current.toString()) } - }, []) + }, [fullStart]) return ( <>
(null) const isDragging = useRef(false); - // Setting Axis Start useEffect(() => { axisStartRef.current = fullStart; @@ -170,23 +168,6 @@ const TimelineAxis: FC<{ }, [handleAxisZoom]) - // useEffect(() => { - // const element = axisRef.current - // if (!element) { - // return - // } - - - - // element.addEventListener('wheel', handleAxisZoom, { passive: false }) - - // // useDebounce(handleAxisZoom, 1000) - // return () => { - // element.removeEventListener('wheel', handleAxisZoom) - // } - // }) - - const handleAxisPanning = (deltaX: number) => { if(!isDragging.current || !timelineWrapperRef?.current) { return; From d0ea61fb0250cb6661d66419f58fb0157a315bb3 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 4 Aug 2026 18:07:37 +0500 Subject: [PATCH 16/26] Cleanup --- src/components/app/Timeline.module.scss | 213 ++++++++++++++---------- src/components/app/Timeline.tsx | 204 +++++++++++++---------- src/hooks/useTimelineData.ts | 31 +--- 3 files changed, 248 insertions(+), 200 deletions(-) diff --git a/src/components/app/Timeline.module.scss b/src/components/app/Timeline.module.scss index ab185a95..9fa57c1b 100644 --- a/src/components/app/Timeline.module.scss +++ b/src/components/app/Timeline.module.scss @@ -2,72 +2,9 @@ @use '../../styles/theming'; @use 'sass:color'; -.detail { - position: relative; - padding: 1em; - background-color: theming.$panelBackgroundColor; - color: theming.$panelTextColor; - border: 1px solid theming.$panelAccentColor; - border-radius: 0.25em; -} - -.detailClose { - position: absolute; - top: 0.5em; - right: 0.5em; - padding: 0.25em; - background: none; - border: none; - cursor: pointer; - color: inherit; -} - -.detailTitle { - display: flex; - align-items: center; - gap: 0.4em; - margin: 0 0 0.25em; - font-size: 1.1em; - - svg { - height: 1em; - } -} - -.detailFeature { - margin: 0 0 0.5em; - opacity: 0.75; -} - -.detailMeta { - display: flex; - flex-wrap: wrap; - gap: 1em; - margin: 0.75em 0 0; - - dt { - font-size: 0.75em; - text-transform: uppercase; - opacity: 0.75; - } - - dd { - margin: 0; - } -} - - - - - - -// NEW NEW NEW - .timelineWrapper { display: flex; flex-direction: column; - // gap: 1px; - // background-color: theming.$panelAccentColor; padding: 1px; } @@ -75,11 +12,9 @@ width: 100%; display: grid; grid-template-columns: 1fr minmax(0, 4fr); - overscroll-behavior: none; gap: 1px; border: 1px solid theming.$panelAccentColor; border-top: none; - // overflow: hidden; } .timelineRow:first-child { @@ -87,57 +22,43 @@ } .cell { - height: 2em; - // background-color: theming.$panelBackgroundColor; - background-color: white; - + height: 3em; } .cell:first-child { border-right: 1px solid theming.$panelAccentColor; } -.workerCell { +.leftCell { display: flex; align-items: center; padding-left: 0.5em; - // font-family: inherit; -} - -.axis { - border:none; + font-family: inherit; } +// Timeline Bar Styles -.workerRow { +.timelineBarWrapper { display: flex; overflow: hidden; align-items: center; - flex-shrink: 0; - // position: absolute; - position: sticky; - // transform: translate3d(200px, 0px, 0px); - // border: 1px solid black; + position: sticky; // Allows setting timelineBar position: absolute } .timelineBar { height: 80%; border-radius: 0.333em; border: none; - padding: 0; - min-width: 0; // Overrides flexbox min-width: auto - box-sizing: border-box; // Ensures borders/padding don't add to the width - flex: none; // Prevents flexbox from growing or shrinking it + box-sizing: border-box; + flex: none; position: absolute; - } @each $name, $color in statuses.$statusColors { .timelineBar[data-status='#{$name}'] { background-color: color.adjust($color, $alpha: -0.666); border-color: $color; - &:hover { outline: 1px solid $color; @@ -146,11 +67,11 @@ &.selected { outline: 1px solid $color } - } - } +// Tooltip Styles + .icon { padding-top: 0.35em; padding-left: 0.5em; @@ -179,4 +100,118 @@ fill: theming.$exampleNumberColor; stroke: theming.$panelAccentColor; stroke-width: 1px; +} + +// Axis Styles + +.axisUnit { + font-size: 0.65em; + text-transform: uppercase; + letter-spacing: 0.05em; + color: theming.$exampleNumberColor; + opacity: 0.9; +} + +.axisRuler { + position: relative; + overflow: hidden; + cursor: grab; + user-select: none; + border: none; + background-color: inherit; +} + +.axisRuler:active { + cursor: grabbing; +} + +.minorTick { + position: absolute; + bottom: 0; + width: 2px; + height: 30%; + background-color: theming.$panelAccentColor; + pointer-events: none; + // Centre the 1 px line on the computed left position + transform: translateX(-50%); +} + +.majorTick { + position: absolute; + bottom: 0; + width: 2px; + height: 55%; + background-color: theming.$exampleNumberColor; + transform: translateX(-50%); + pointer-events: none; +} + +.tickLabel { + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%); + font-size: 0.6em; + line-height: 1.5; + white-space: nowrap; + color: theming.$panelTextColor; + opacity: 0.75; + pointer-events: none; +} + + +// Timeline Bar Detail Styles + +.detail { + position: relative; + padding: 1em; + background-color: theming.$panelBackgroundColor; + color: theming.$panelTextColor; + border: 1px solid theming.$panelAccentColor; + border-radius: 0.25em; +} + +.detailClose { + position: absolute; + top: 0.5em; + right: 0.5em; + padding: 0.25em; + background: none; + border: none; + cursor: pointer; + color: inherit; +} + +.detailTitle { + display: flex; + align-items: center; + gap: 0.4em; + margin: 0 0 0.25em; + font-size: 1.1em; + + svg { + height: 1em; + } +} + +.detailFeature { + margin: 0 0 0.5em; + opacity: 0.75; +} + +.detailMeta { + display: flex; + flex-wrap: wrap; + gap: 1em; + margin: 0.75em 0 0; + + dt { + font-size: 0.75em; + text-transform: uppercase; + opacity: 0.75; + } + + dd { + margin: 0; + } } \ No newline at end of file diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index 2c8e58ec..4baf9ad1 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -1,7 +1,8 @@ import { faXmark } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { type FC, useEffect, useRef, useState } from 'react' +import { type FC, useEffect, useMemo, useRef, useState } from 'react' import { Button, OverlayArrow, Tooltip, TooltipTrigger } from 'react-aria-components' +import { useDebouncedCallback } from 'use-debounce' import { formatExecutionDuration } from '../../formatExecutionDuration.js' import { type TimelineItem, useTimelineData } from '../../hooks/useTimelineData.js' import { StatusIcon } from '../gherkin/StatusIcon.js' @@ -9,35 +10,37 @@ import statusName from '../gherkin/statusName.js' import { Tags } from '../gherkin/Tags.js' import { TestCaseOutcome } from '../results/index.js' import styles from './Timeline.module.scss' -import { useDebouncedCallback } from 'use-debounce' type unit = { label: string magnitude: number } -const axisUnits: unit[] = [ - { label: '1 ms', magnitude: 1 }, - { label: '10 ms', magnitude: 10 }, - { label: '50 ms', magnitude: 50 }, - { label: '100 ms', magnitude: 100 }, - { label: '500 ms', magnitude: 500 }, - { label: '1 s', magnitude: 1 * 1000 }, - { label: '10 s', magnitude: 10 * 1000 }, - { label: '30 s', magnitude: 30 * 1000 }, - { label: '1 min', magnitude: 1 * 60 * 1000 }, - { label: '10 min', magnitude: 10 * 60 * 1000 }, - { label: '30 min', magnitude: 30 * 60 * 1000 }, - { label: '1 hr', magnitude: 1 * 60 * 60 * 1000 }, -] - const pxPerUnit = 2 +const AXIS_CONFIG = { + minorInterval: 5, // Minor tick every N × magnitude ms + majorInterval: 50, // Major tick every N × magnitude ms (must be a multiple of minorInterval) +} + +const axisUnits: unit[] = [ + { label: `${1 * AXIS_CONFIG.minorInterval} ms`, magnitude: 1 }, + { label: `${10 * AXIS_CONFIG.minorInterval} ms`, magnitude: 10 }, + { label: `${50 * AXIS_CONFIG.minorInterval} ms`, magnitude: 50 }, + { label: `${100 * AXIS_CONFIG.minorInterval} ms`, magnitude: 100 }, + { label: `${500 * AXIS_CONFIG.minorInterval} ms`, magnitude: 500 }, + { label: `${1 * AXIS_CONFIG.minorInterval} s`, magnitude: 1 * 1000 }, + { label: `${10 * AXIS_CONFIG.minorInterval} s`, magnitude: 10 * 1000 }, + { label: `${30 * AXIS_CONFIG.minorInterval} s`, magnitude: 30 * 1000 }, + { label: `${1 * AXIS_CONFIG.minorInterval} min`, magnitude: 1 * 60 * 1000 }, + { label: `${10 * AXIS_CONFIG.minorInterval} min`, magnitude: 10 * 60 * 1000 }, + { label: `${30 * AXIS_CONFIG.minorInterval} min`, magnitude: 30 * 60 * 1000 }, + { label: `${1 * AXIS_CONFIG.minorInterval} hr`, magnitude: 1 * 60 * 60 * 1000 }, +] export const Timeline: FC = () => { const { groups, items, fullStart, fullEnd } = useTimelineData() const [selectedId, setSelectedId] = useState(undefined) - const [rowWidthPx, setRowWidthPx] = useState(0); - + const timelineWrapperRef = useRef(null) const axisStartRef = useRef(fullStart) const axisUnitRef = useRef(0) @@ -47,7 +50,10 @@ export const Timeline: FC = () => { useEffect(() => { if (timelineWrapperRef.current) { console.log(`${fullStart} loaded`) - timelineWrapperRef.current.style.setProperty('--magnitude', axisUnits[axisUnitRef.current].magnitude.toString()) + timelineWrapperRef.current.style.setProperty( + '--magnitude', + axisUnits[axisUnitRef.current].magnitude.toString() + ) timelineWrapperRef.current.style.setProperty('--axis-start', axisStartRef.current.toString()) } }, [fullStart]) @@ -57,7 +63,6 @@ export const Timeline: FC = () => {
{ {groups.map((grp) => { return (
-
{grp.label}
+
{grp.label}
- -
+
{items .filter((i) => i.groupId === grp.id) .map((item) => { - if (rowWidthPx === 0) { - return null - } return ( { } const TimelineAxis: FC<{ - setRowWidthPx: React.Dispatch> fullStart: number fullEnd: number axisStartRef: React.RefObject timelineWrapperRef: React.RefObject axisUnitRef: React.RefObject -}> = ({ setRowWidthPx, fullStart, fullEnd, axisStartRef, timelineWrapperRef, axisUnitRef }) => { +}> = ({ fullStart, fullEnd, axisStartRef, timelineWrapperRef, axisUnitRef }) => { + const [currentUnitIndex, setCurrentUnitIndex] = useState(0) + const axisRef = useRef(null) - const isDragging = useRef(false); + const isDragging = useRef(false) useEffect(() => { + axisStartRef.current = fullStart + axisUnitRef.current = 0 + setCurrentUnitIndex(0) + }, [axisStartRef, fullStart, axisUnitRef]) - axisStartRef.current = fullStart; - axisUnitRef.current = 0; - - const element = axisRef.current - if (!element) { - return - } - - const observer = new ResizeObserver((entries) => { - for (const entry of entries) { - setRowWidthPx(entry.contentRect.width) - } - }) - - observer.observe(element) - return () => observer.disconnect() - }, [setRowWidthPx, axisStartRef, fullStart, axisUnitRef]) - - // Regisetering Handle Zoom Callback const handleAxisZoom = useDebouncedCallback((deltaY: number) => { - if(!timelineWrapperRef?.current || !axisRef.current) { - return; + if (!timelineWrapperRef?.current) { + return } const zoomDirection = deltaY < 0 ? -1 : 1 - let newAxisUnitIndex = 0; - if (zoomDirection === 1) { - newAxisUnitIndex = Math.min(axisUnits.length - 1, axisUnitRef.current + 1) - } else { - newAxisUnitIndex = Math.max(0, axisUnitRef.current - 1) - } + const newIndex = + zoomDirection === 1 + ? Math.min(axisUnits.length - 1, axisUnitRef.current + 1) + : Math.max(0, axisUnitRef.current - 1) - axisUnitRef.current = newAxisUnitIndex; - - axisRef.current.textContent = `Axis Unit: ${axisUnits[newAxisUnitIndex].label}` - timelineWrapperRef.current.style.setProperty('--magnitude', axisUnits[newAxisUnitIndex].magnitude.toString()); + axisUnitRef.current = newIndex + timelineWrapperRef.current.style.setProperty( + '--magnitude', + axisUnits[newIndex].magnitude.toString() + ) + setCurrentUnitIndex(newIndex) }, 100) useEffect(() => { @@ -167,28 +154,68 @@ const TimelineAxis: FC<{ } }, [handleAxisZoom]) - const handleAxisPanning = (deltaX: number) => { - if(!isDragging.current || !timelineWrapperRef?.current) { - return; - } - - let newStart = 0; - if(deltaX < 0) { - newStart = Math.min(fullEnd, axisStartRef.current + axisUnits[axisUnitRef.current].magnitude) - } else { - newStart = Math.max(fullStart, axisStartRef.current - axisUnits[axisUnitRef.current].magnitude) + if (!isDragging.current || !timelineWrapperRef?.current) { + return } - axisStartRef.current = newStart; - timelineWrapperRef.current.style.setProperty('--axis-start', newStart.toString()); - } + const newStart = + deltaX < 0 + ? Math.min(fullEnd, axisStartRef.current + axisUnits[axisUnitRef.current].magnitude * 5) + : Math.max(fullStart, axisStartRef.current - axisUnits[axisUnitRef.current].magnitude * 5) + + axisStartRef.current = newStart + timelineWrapperRef.current.style.setProperty('--axis-start', newStart.toString()) + } + + const ticks = useMemo(() => { + const magnitude = axisUnits[currentUnitIndex].magnitude + const minorInterval = AXIS_CONFIG.minorInterval * magnitude + const minorStepsPerMajor = AXIS_CONFIG.majorInterval / AXIS_CONFIG.minorInterval + + const firstK = Math.floor(fullStart / minorInterval) + const lastK = Math.ceil(fullEnd / minorInterval) + + const result: Array<{ time: number; isMajor: boolean }> = [] + for (let k = firstK; k <= lastK; k++) { + const time = k * minorInterval + const isMajor = k % minorStepsPerMajor === 0 + result.push({ time, isMajor }) + } + return result + }, [currentUnitIndex, fullStart, fullEnd]) return (
-
-
) @@ -204,9 +231,12 @@ const TimelineBar: FC<{ @@ -241,15 +271,15 @@ const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item,
Status
{statusName(item.status)}
-
+
Start
{item.start}
-
-
End
-
{item.end}
-
+
+
End
+
{item.end}
+
Duration
{formatExecutionDuration(new Date(item.start), new Date(item.end))}
@@ -262,3 +292,9 @@ const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item,
) } + +function formatTime(time: number): string { + const d = new Date(time) + const formattedTime = `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}:${d.getMilliseconds()}` + return formattedTime +} \ No newline at end of file diff --git a/src/hooks/useTimelineData.ts b/src/hooks/useTimelineData.ts index c5b07fa3..2dadcd0e 100644 --- a/src/hooks/useTimelineData.ts +++ b/src/hooks/useTimelineData.ts @@ -7,7 +7,6 @@ import { import { useMemo } from 'react' import { useFilteredTestCases } from './useFilteredTestCases.js' import { useQueries } from './useQueries.js' -import { useSearch } from './useSearch.js' export interface TimelineItem { readonly id: string @@ -32,24 +31,20 @@ export interface TimelineData { readonly items: readonly TimelineItem[] readonly fullStart: number readonly fullEnd: number - readonly filtered: boolean } const UNASSIGNED_GROUP_ID = '' export function useTimelineData(): TimelineData { const { cucumberQuery } = useQueries() - const { hideStatuses, tagExpression, searchTerm, unchanged } = useSearch() const finishedTestCases = useFilteredTestCases() return useMemo(() => { const items: TimelineItem[] = [] const groupIds = new Set() - // const normalizedSearchTerm = searchTerm?.trim().toLowerCase() - let fullStart: number = Number.MAX_SAFE_INTEGER; - let fullEnd: number = Number.MIN_SAFE_INTEGER; + let fullStart: number = Number.MAX_SAFE_INTEGER + let fullEnd: number = Number.MIN_SAFE_INTEGER - // for (const testCaseFinished of cucumberQuery.findAllTestCaseFinished()) { for (const testCaseFinished of finishedTestCases) { const testCaseStarted = cucumberQuery.findTestCaseStartedBy(testCaseFinished.testCaseEvent) if (!testCaseStarted) { @@ -77,27 +72,9 @@ export function useTimelineData(): TimelineData { cucumberQuery.findMostSevereTestStepResultBy(testCaseFinished.testCaseEvent)?.status ?? TestStepResultStatus.PASSED - // if (hideStatuses.includes(status)) { - // continue - // } - - // if (tagExpression) { - // const tagNames = pickle.tags.map((tag) => tag.name) - // if (!tagExpression.evaluate(tagNames)) { - // continue - // } - // } - const feature = testCaseFinished.lineage.feature?.name ?? '' const scenario = pickle.name - // if ( - // normalizedSearchTerm && - // !`${feature} ${scenario}`.toLowerCase().includes(normalizedSearchTerm) - // ) { - // continue - // } - const groupId = testCaseStarted.workerId ?? UNASSIGNED_GROUP_ID groupIds.add(groupId) @@ -121,8 +98,8 @@ export function useTimelineData(): TimelineData { .sort(compareGroupIds) .map((id) => ({ id, label: describeGroup(id) })) - return { groups, items, fullStart, fullEnd, filtered: !unchanged } - }, [cucumberQuery, unchanged, finishedTestCases]) + return { groups, items, fullStart, fullEnd } + }, [cucumberQuery, finishedTestCases]) } function describeGroup(id: string): string { From dc6960bac61b5934dd099eb7893a34020258f4fd Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 4 Aug 2026 20:20:58 +0500 Subject: [PATCH 17/26] Refactored Timeline Bar --- src/components/app/Timeline.module.scss | 12 ++++--- src/components/app/Timeline.tsx | 48 ++++++++++++++----------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/src/components/app/Timeline.module.scss b/src/components/app/Timeline.module.scss index 9fa57c1b..dd4bf825 100644 --- a/src/components/app/Timeline.module.scss +++ b/src/components/app/Timeline.module.scss @@ -80,6 +80,9 @@ // Tooltip card :global(.react-aria-Tooltip) { + display: flex; + flex-direction: column; + overflow: hidden; background-color: theming.$panelBackgroundColor; color: theming.$panelTextColor; border: 1px solid theming.$panelAccentColor; @@ -87,7 +90,6 @@ padding: 0.4em 0.6em; font-size: inherit; font-family: inherit; - display: flex; align-items: center; gap: 0.4em; box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); @@ -95,6 +97,11 @@ margin-bottom: 0.7em; } +.tooltipBtn { + border: none; + background: none; +} + // Arrow styling :global(.react-aria-Tooltip) svg { fill: theming.$exampleNumberColor; @@ -132,8 +139,6 @@ height: 30%; background-color: theming.$panelAccentColor; pointer-events: none; - // Centre the 1 px line on the computed left position - transform: translateX(-50%); } .majorTick { @@ -142,7 +147,6 @@ width: 2px; height: 55%; background-color: theming.$exampleNumberColor; - transform: translateX(-50%); pointer-events: none; } diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index 4baf9ad1..85f25510 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -81,7 +81,7 @@ export const Timeline: FC = () => { return ( @@ -161,8 +161,8 @@ const TimelineAxis: FC<{ const newStart = deltaX < 0 - ? Math.min(fullEnd, axisStartRef.current + axisUnits[axisUnitRef.current].magnitude * 5) - : Math.max(fullStart, axisStartRef.current - axisUnits[axisUnitRef.current].magnitude * 5) + ? Math.min(fullEnd + AXIS_CONFIG.majorInterval * axisUnits[axisUnitRef.current].magnitude, axisStartRef.current + axisUnits[axisUnitRef.current].magnitude * 5) + : Math.max(fullStart - AXIS_CONFIG.majorInterval * axisUnits[axisUnitRef.current].magnitude, axisStartRef.current - axisUnits[axisUnitRef.current].magnitude * 5) axisStartRef.current = newStart timelineWrapperRef.current.style.setProperty('--axis-start', newStart.toString()) @@ -222,33 +222,41 @@ const TimelineAxis: FC<{ } const TimelineBar: FC<{ - item: TimelineItem + items: TimelineItem[] selectedId: string | undefined - setSelectedId: (id: string) => void -}> = ({ item, selectedId, setSelectedId }) => { - return ( - + setSelectedId: (id: string | undefined) => void +}> = ({ items, selectedId, setSelectedId }) => { + + const start = items.reduce((acc, item) => Math.min(acc, item.start) , Number.MAX_SAFE_INTEGER) + const end = items.reduce((acc, item) => Math.max(acc, item.end) , Number.MIN_SAFE_INTEGER) + const status = items.length === 1 ? items[0].status: 'undefined' + + return items.length && ( + - Tooltip Arrow + Tooltip Arrow - - - - {item.scenario} + {items.map(item => { + return + })} ) @@ -273,12 +281,12 @@ const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item,
Start
-
{item.start}
+
{formatTime(item.start)}
End
-
{item.end}
+
{formatTime(item.end)}
Duration
{formatExecutionDuration(new Date(item.start), new Date(item.end))}
From 4c12d23cca1f5b0acba9028dac3dd31866a49f69 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 4 Aug 2026 20:41:58 +0500 Subject: [PATCH 18/26] Refactored useTimeline hook --- src/components/app/Timeline.tsx | 8 ++++---- src/hooks/useTimelineData.ts | 28 +++++++++++++++++----------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index 85f25510..40b37974 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -38,14 +38,14 @@ const axisUnits: unit[] = [ { label: `${1 * AXIS_CONFIG.minorInterval} hr`, magnitude: 1 * 60 * 60 * 1000 }, ] export const Timeline: FC = () => { - const { groups, items, fullStart, fullEnd } = useTimelineData() + const { groups, fullStart, fullEnd } = useTimelineData() const [selectedId, setSelectedId] = useState(undefined) const timelineWrapperRef = useRef(null) const axisStartRef = useRef(fullStart) const axisUnitRef = useRef(0) - const selectedItem = items.find((item) => item.id === selectedId) + let selectedItem: TimelineItem | null = null useEffect(() => { if (timelineWrapperRef.current) { @@ -75,9 +75,9 @@ export const Timeline: FC = () => {
{grp.label}
- {items - .filter((i) => i.groupId === grp.id) + {grp.items .map((item) => { + selectedItem = selectedId === item.id ? item: selectedItem return ( { - const items: TimelineItem[] = [] - const groupIds = new Set() + const groupMap: Record = {} let fullStart: number = Number.MAX_SAFE_INTEGER let fullEnd: number = Number.MIN_SAFE_INTEGER @@ -76,9 +75,9 @@ export function useTimelineData(): TimelineData { const scenario = pickle.name const groupId = testCaseStarted.workerId ?? UNASSIGNED_GROUP_ID - groupIds.add(groupId) - items.push({ + + const item = { id: testCaseStarted.id, groupId, groupLabel: describeGroup(groupId), @@ -89,16 +88,23 @@ export function useTimelineData(): TimelineData { start: itemStart, end: itemEnd, testCaseStarted, - }) + } + + if(groupMap[groupId]) { + groupMap[groupId].items.push(item) + } else { + groupMap[groupId] = {id: groupId, label: describeGroup(groupId), items: [item]} + } } - items.sort((a, b) => a.start - b.start || a.end - b.end) + for(const grp of Object.values(groupMap)) { + grp.items.sort((a, b) => a.start - b.start || a.end - b.end) + } - const groups: TimelineGroup[] = [...groupIds] - .sort(compareGroupIds) - .map((id) => ({ id, label: describeGroup(id) })) + const groups: TimelineGroup[] = Object.values(groupMap); + groups.sort((a, b) => compareGroupIds(a.id, b.id)) - return { groups, items, fullStart, fullEnd } + return { groups, fullStart, fullEnd } }, [cucumberQuery, finishedTestCases]) } From d04d642fa549873742dbf718d744e1ff6483a6c6 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 4 Aug 2026 21:31:38 +0500 Subject: [PATCH 19/26] Implemented items bucket --- src/components/app/Timeline.tsx | 55 +++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index 40b37974..031f8e2d 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -40,6 +40,7 @@ const axisUnits: unit[] = [ export const Timeline: FC = () => { const { groups, fullStart, fullEnd } = useTimelineData() const [selectedId, setSelectedId] = useState(undefined) + const [currentUnitIndex, setCurrentUnitIndex] = useState(0) const timelineWrapperRef = useRef(null) const axisStartRef = useRef(fullStart) @@ -65,6 +66,8 @@ export const Timeline: FC = () => { axisUnitRef={axisUnitRef} fullStart={fullStart} fullEnd={fullEnd} + currentUnitIndex={currentUnitIndex} + setCurrentUnitIndex={setCurrentUnitIndex} timelineWrapperRef={timelineWrapperRef} axisStartRef={axisStartRef} > @@ -75,13 +78,14 @@ export const Timeline: FC = () => {
{grp.label}
- {grp.items - .map((item) => { - selectedItem = selectedId === item.id ? item: selectedItem + {bucketItems(grp.items, axisUnits[axisUnitRef.current ?? 0].magnitude) + .map((itemIds) => { + // selectedItem = selectedId === itemIds.id ? item: selectedItem + itemIds.forEach((id) => {selectedItem = grp.items[id].id === selectedId ? grp.items[id]: selectedItem} ) return ( @@ -102,11 +106,13 @@ export const Timeline: FC = () => { const TimelineAxis: FC<{ fullStart: number fullEnd: number + currentUnitIndex: number + setCurrentUnitIndex: React.Dispatch> axisStartRef: React.RefObject timelineWrapperRef: React.RefObject axisUnitRef: React.RefObject -}> = ({ fullStart, fullEnd, axisStartRef, timelineWrapperRef, axisUnitRef }) => { - const [currentUnitIndex, setCurrentUnitIndex] = useState(0) +}> = ({ fullStart, fullEnd, currentUnitIndex, setCurrentUnitIndex, axisStartRef, timelineWrapperRef, axisUnitRef }) => { + // const [currentUnitIndex, setCurrentUnitIndex] = useState(0) const axisRef = useRef(null) const isDragging = useRef(false) @@ -115,7 +121,7 @@ const TimelineAxis: FC<{ axisStartRef.current = fullStart axisUnitRef.current = 0 setCurrentUnitIndex(0) - }, [axisStartRef, fullStart, axisUnitRef]) + }, [axisStartRef, fullStart, axisUnitRef, setCurrentUnitIndex]) const handleAxisZoom = useDebouncedCallback((deltaY: number) => { if (!timelineWrapperRef?.current) { @@ -229,7 +235,7 @@ const TimelineBar: FC<{ const start = items.reduce((acc, item) => Math.min(acc, item.start) , Number.MAX_SAFE_INTEGER) const end = items.reduce((acc, item) => Math.max(acc, item.end) , Number.MIN_SAFE_INTEGER) - const status = items.length === 1 ? items[0].status: 'undefined' + const status = items.length === 1 ? items[0].status: 'UNKNOWN' return items.length && ( @@ -305,4 +311,35 @@ function formatTime(time: number): string { const d = new Date(time) const formattedTime = `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}:${d.getMilliseconds()}` return formattedTime +} + +function bucketItems(items: TimelineItem[], minDuration: number) { + const result: number[][] = []; + + let i = 0 + while(i < items.length) { + const bucket: number[] = [] + let bucketDuration = 0 + + do { + bucket.push(i) + bucketDuration += items[i].end - items[i].start + 1 + i++ + } while(i < items.length && bucketDuration < minDuration) + + + if(bucketDuration >= minDuration) { + result.push(bucket) + } else { + // Try merging with left + if(result.length > 0) { + result[result.length - 1].push(...bucket) + } else { + // No adjacent bucket exist + result.push(bucket) + } + } + } + + return result; } \ No newline at end of file From 7915f17a0a8950c4b9a035080b137ba5bb6283d9 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 4 Aug 2026 22:46:01 +0500 Subject: [PATCH 20/26] Uninstalled vis-data and vis-timeline --- package-lock.json | 152 +--------------------------------------------- package.json | 4 +- 2 files changed, 2 insertions(+), 154 deletions(-) diff --git a/package-lock.json b/package-lock.json index aeb747cb..f2086ca9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,9 +31,7 @@ "rehype-sanitize": "6.0.0", "remark-breaks": "4.0.0", "remark-gfm": "4.0.1", - "use-debounce": "^10.0.0", - "vis-data": "^8.0.4", - "vis-timeline": "^8.5.1" + "use-debounce": "^10.0.0" }, "devDependencies": { "@biomejs/biome": "^2.4.1", @@ -942,19 +940,6 @@ "integrity": "sha512-uap3XSQFxj5HYAHQIShGeS2zotMEnUmnEVjyuhp39j7tDUvaU64ArHzRkLPSWRavEI8ycdeNdwef1pcM/n6pSQ==", "license": "MIT" }, - "node_modules/@egjs/hammerjs": { - "version": "2.0.17", - "resolved": "https://registry.npmjs.org/@egjs/hammerjs/-/hammerjs-2.0.17.tgz", - "integrity": "sha512-XQsZgjm2EcVUiZQf11UBJQfmZeEmOW8DpI1gsFeln6w0ae0ii4dMQEQ0kjl6DspdWX1aGY1/loyXnP0JS06e/A==", - "license": "MIT", - "peer": true, - "dependencies": { - "@types/hammerjs": "^2.0.36" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.4", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", @@ -6270,16 +6255,6 @@ "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "dev": true }, - "node_modules/component-emitter": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.1.tgz", - "integrity": "sha512-T0+barUSQRTUQASh8bx02dl+DhF54GtIDY13Y3m9oWTklKbb3Wv974meRpeZ3lp1JpLVECWWNHC4vaG2XHXouQ==", - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -6493,13 +6468,6 @@ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/cssfilter": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", - "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==", - "license": "MIT", - "peer": true - }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -8763,13 +8731,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/keycharm": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/keycharm/-/keycharm-0.4.0.tgz", - "integrity": "sha512-TyQTtsabOVv3MeOpR92sIKk/br9wxS+zGj4BG7CR8YbK4jM3tyIBaF0zhzeBUMx36/Q/iQLOKKOT+3jOQtemRQ==", - "license": "(Apache-2.0 OR MIT)", - "peer": true - }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -10652,16 +10613,6 @@ "node": ">=12" } }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "peer": true, - "engines": { - "node": "*" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -11468,16 +11419,6 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "dev": true }, - "node_modules/propagating-hammerjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/propagating-hammerjs/-/propagating-hammerjs-3.0.0.tgz", - "integrity": "sha512-FJTclGll0ysatpF9rKO4jwobyaVDitPb0g/bGlufqqtXPQX8mxf8IXilnIK2iYRMPkVlYeFNhPTrspF9CM1stg==", - "license": "MIT", - "peer": true, - "peerDependencies": { - "@egjs/hammerjs": "^2.0.17" - } - }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", @@ -13340,20 +13281,6 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/uuid": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", - "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", - "funding": [ - "https://github.com/sponsors/broofa", - "https://github.com/sponsors/ctavan" - ], - "license": "MIT", - "peer": true, - "bin": { - "uuid": "dist-node/bin/uuid" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -13370,59 +13297,6 @@ "node": ">= 0.8" } }, - "node_modules/vis-data": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/vis-data/-/vis-data-8.0.4.tgz", - "integrity": "sha512-TsN0sMHqIRpdfg6TNPtfdINpkgxtnQP6JNWCaiSwvou5seXqKiP5eERkaBg+Y56wyJ4FZTeOEs/dEmWEPrpltQ==", - "license": "(Apache-2.0 OR MIT)", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/visjs" - }, - "peerDependencies": { - "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^13.0.0 || ^14.0.0", - "vis-util": ">=6.0.0" - } - }, - "node_modules/vis-timeline": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/vis-timeline/-/vis-timeline-8.5.1.tgz", - "integrity": "sha512-6pqx4Zl/xHCEy5nXRaz9xCOx1HtZWkxSIt1oHJmDZy/UxT09kwq1eA4eKRAzhHey3PN1Ee3DsxuxHm7yI2G2mQ==", - "license": "(Apache-2.0 OR MIT)", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/visjs" - }, - "peerDependencies": { - "@egjs/hammerjs": "^2.0.0", - "component-emitter": "^1.3.0", - "keycharm": "^0.2.0 || ^0.3.0 || ^0.4.0", - "moment": "^2.24.0", - "propagating-hammerjs": "^1.4.0 || ^2.0.0 || ^3.0.0", - "uuid": "^3.4.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0 || ^13.0.0 || ^14.0.0", - "vis-data": ">=8.0.0", - "vis-util": ">=6.0.0", - "xss": "^1.0.0" - } - }, - "node_modules/vis-util": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/vis-util/-/vis-util-6.0.0.tgz", - "integrity": "sha512-qtpts3HRma0zPe4bO7t9A2uejkRNj8Z2Tb6do6lN85iPNWExFkUiVhdAq5uLGIUqBFduyYeqWJKv/jMkxX0R5g==", - "license": "(Apache-2.0 OR MIT)", - "peer": true, - "engines": { - "node": ">=8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/visjs" - }, - "peerDependencies": { - "@egjs/hammerjs": "^2.0.0", - "component-emitter": "^1.3.0 || ^2.0.0" - } - }, "node_modules/vite": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", @@ -13810,30 +13684,6 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, - "node_modules/xss": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", - "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", - "license": "MIT", - "peer": true, - "dependencies": { - "commander": "^2.20.3", - "cssfilter": "0.0.10" - }, - "bin": { - "xss": "bin/xss" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/xss/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT", - "peer": true - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index f9957282..947f9b8a 100644 --- a/package.json +++ b/package.json @@ -48,9 +48,7 @@ "rehype-sanitize": "6.0.0", "remark-breaks": "4.0.0", "remark-gfm": "4.0.1", - "use-debounce": "^10.0.0", - "vis-data": "^8.0.4", - "vis-timeline": "^8.5.1" + "use-debounce": "^10.0.0" }, "peerDependencies": { "react": "^18.0.0 || ^19.0.0", From e3ff6793a0d72ba260f41ab87789b23fa16bf012 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 4 Aug 2026 23:04:57 +0500 Subject: [PATCH 21/26] Resolved Merge Conflicts --- CHANGELOG.md | 2 +- package-lock.json | 16 +++ src/components/app/Timeline.spec.tsx | 161 ------------------------ src/custom.d.ts | 2 - src/hooks/useFilteredTestCases.spec.tsx | 4 +- src/hooks/useFilteredTestCases.ts | 1 - 6 files changed, 18 insertions(+), 168 deletions(-) delete mode 100644 src/components/app/Timeline.spec.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 84d10cf7..b6c59818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -586,4 +586,4 @@ to rebuild them every time the envelope list is updated. Use this instead of `` component showing scenario execution over time grouped by worker ported from cucumber-jvm's TimelineFormatter ([#126](https://github.com/cucumber/react-components/issues/126)) +- Added `` component showing scenario execution over time grouped by worker ([#126](https://github.com/cucumber/react-components/issues/126)) diff --git a/package-lock.json b/package-lock.json index f2086ca9..3e1f1658 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13413,6 +13413,22 @@ } } }, + "node_modules/vite-tsconfig-paths/node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/vite/node_modules/fdir": { "version": "6.4.4", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.4.tgz", diff --git a/src/components/app/Timeline.spec.tsx b/src/components/app/Timeline.spec.tsx deleted file mode 100644 index e1186a9f..00000000 --- a/src/components/app/Timeline.spec.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { type Envelope, type TestCaseStarted, TestStepResultStatus } from '@cucumber/messages' -import { fireEvent, render, screen, within } from '@testing-library/react' -import { userEvent } from '@testing-library/user-event' -import { expect } from 'chai' - -import examplesTablesFeature from '../../../acceptance/examples-tables/examples-tables.js' -import { ControlledSearchProvider } from './ControlledSearchProvider.js' -import { EnvelopesProvider } from './EnvelopesProvider.js' -import { Timeline } from './Timeline.js' - -describe('', () => { - it('should show a message when no scenarios were executed', () => { - render( - - {}}> - - - - ) - - expect(screen.getByText('No scenarios were executed.')).to.be.visible - }) - - it('should show a message when filters exclude every scenario', () => { - render( - - {}} - > - - - - ) - - expect(screen.getByText('No scenarios match your query and/or filters.')).to.be.visible - }) - - it('should render one bar per executed scenario, in a single lane when no worker information is present', () => { - render( - - {}}> - - - - ) - - expect(screen.getByText('Main process')).to.be.visible - expect(screen.getAllByRole('button')).to.have.length(7) - }) - - it('should respect the hideStatuses filter from the shared search context', () => { - render( - - {}} - > - - - - ) - - expect(screen.getAllByRole('button')).to.have.length(5) - }) - - it('should respect a tag expression from the shared search context', () => { - render( - - {}} - > - - - - ) - - expect(screen.getAllByRole('button')).to.have.length(2) - }) - - it('should group test cases by worker id, sorted numerically', () => { - render( - - {}}> - - - - ) - - expect(screen.getAllByTestId('cucumber.timeline.group')).to.have.length(2) - expect(screen.getByText('Worker 0')).to.be.visible - expect(screen.getByText('Worker 1')).to.be.visible - }) - - it('should render an axis ruler that updates when zooming and panning', () => { - render( - - {}}> - - - - ) - - const axis = screen.getByRole('button', { name: 'Timeline axis scale 10 ms' }) - - expect(within(axis).getByText('Scale')).to.be.visible - expect(within(axis).getByText('0 seconds')).to.be.visible - - fireEvent.wheel(axis, { deltaY: 1 }) - - expect(screen.getByRole('button', { name: 'Timeline axis scale 50 ms' })).to.be.visible - - fireEvent.mouseDown(axis) - fireEvent.mouseMove(axis, { movementX: -20 }) - fireEvent.mouseUp(axis) - - expect(within(axis).getByText('0.01 seconds')).to.be.visible - }) - - it('should show scenario details when a bar is selected, and hide them again on close', async () => { - render( - - {}}> - - - - ) - - expect(screen.queryByTestId('cucumber.timeline.detail')).to.be.null - - await userEvent.click(screen.getByRole('button', { name: 'Eating cucumbers with 11 friends' })) - - const detail = screen.getByTestId('cucumber.timeline.detail') - expect(within(detail).getByText('Eating cucumbers with 11 friends')).to.be.visible - expect(within(detail).getByText('Examples Tables')).to.be.visible - - await userEvent.click(within(detail).getByRole('button', { name: 'Close' })) - - expect(screen.queryByTestId('cucumber.timeline.detail')).to.be.null - }) -}) - -function distributeAcrossWorkers( - envelopes: ReadonlyArray, - workerCount: number -): ReadonlyArray { - let index = 0 - return envelopes.map((envelope): Envelope => { - if (!envelope.testCaseStarted) { - return envelope - } - const workerId = String(index % workerCount) - index += 1 - const testCaseStarted: TestCaseStarted = { ...envelope.testCaseStarted, workerId } - return { ...envelope, testCaseStarted } - }) -} diff --git a/src/custom.d.ts b/src/custom.d.ts index 14a684e2..e48f665b 100644 --- a/src/custom.d.ts +++ b/src/custom.d.ts @@ -2,5 +2,3 @@ declare module '*.module.scss' { const classes: { [key: string]: string } export default classes } - -declare module '*.css' diff --git a/src/hooks/useFilteredTestCases.spec.tsx b/src/hooks/useFilteredTestCases.spec.tsx index ce4fb2cb..5ea17fc8 100644 --- a/src/hooks/useFilteredTestCases.spec.tsx +++ b/src/hooks/useFilteredTestCases.spec.tsx @@ -5,7 +5,6 @@ import { expect } from 'chai' import attachments from '../../acceptance/attachments/attachments.js' import backgrounds from '../../acceptance/backgrounds/backgrounds.js' import hooksConditional from '../../acceptance/hooks-conditional/hooks-conditional.js' -import parallel from '../../acceptance/parallel/parallel.js' import retry from '../../acceptance/retry/retry.js' import rules from '../../acceptance/rules/rules.js' import { EnvelopesProvider } from '../components/app/EnvelopesProvider.js' @@ -40,9 +39,8 @@ function renderAndExtractPickleNames({ describe('useFilteredTestCases', () => { describe('with no filters', () => { it('returns a test case for every finished scenario', async () => { - // const { result } = renderAndExtractPickleNames({ envelopes: hooksConditional }) + const { result } = renderAndExtractPickleNames({ envelopes: hooksConditional }) - const { result } = renderAndExtractPickleNames({ envelopes: parallel }) await waitFor(() => expect(result.current).to.have.members([ 'A failure in the before hook and a skipped step', diff --git a/src/hooks/useFilteredTestCases.ts b/src/hooks/useFilteredTestCases.ts index 54c8ffa6..d80add38 100644 --- a/src/hooks/useFilteredTestCases.ts +++ b/src/hooks/useFilteredTestCases.ts @@ -24,7 +24,6 @@ export function useFilteredTestCases(): ReadonlyArray>>([]) From f3b3b3c8d1c3e6233cb0548f8e9006c11301a673 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Tue, 4 Aug 2026 23:06:58 +0500 Subject: [PATCH 22/26] Fixed formatting --- src/components/app/Timeline.tsx | 117 +++++++++++++++++++------------- src/hooks/useTimelineData.ts | 9 ++- 2 files changed, 75 insertions(+), 51 deletions(-) diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index 031f8e2d..7ec50752 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -78,10 +78,12 @@ export const Timeline: FC = () => {
{grp.label}
- {bucketItems(grp.items, axisUnits[axisUnitRef.current ?? 0].magnitude) - .map((itemIds) => { + {bucketItems(grp.items, axisUnits[axisUnitRef.current ?? 0].magnitude).map( + (itemIds) => { // selectedItem = selectedId === itemIds.id ? item: selectedItem - itemIds.forEach((id) => {selectedItem = grp.items[id].id === selectedId ? grp.items[id]: selectedItem} ) + itemIds.forEach((id) => { + selectedItem = grp.items[id].id === selectedId ? grp.items[id] : selectedItem + }) return ( { setSelectedId={setSelectedId} > ) - })} + } + )}
) @@ -111,7 +114,15 @@ const TimelineAxis: FC<{ axisStartRef: React.RefObject timelineWrapperRef: React.RefObject axisUnitRef: React.RefObject -}> = ({ fullStart, fullEnd, currentUnitIndex, setCurrentUnitIndex, axisStartRef, timelineWrapperRef, axisUnitRef }) => { +}> = ({ + fullStart, + fullEnd, + currentUnitIndex, + setCurrentUnitIndex, + axisStartRef, + timelineWrapperRef, + axisUnitRef, +}) => { // const [currentUnitIndex, setCurrentUnitIndex] = useState(0) const axisRef = useRef(null) @@ -167,8 +178,14 @@ const TimelineAxis: FC<{ const newStart = deltaX < 0 - ? Math.min(fullEnd + AXIS_CONFIG.majorInterval * axisUnits[axisUnitRef.current].magnitude, axisStartRef.current + axisUnits[axisUnitRef.current].magnitude * 5) - : Math.max(fullStart - AXIS_CONFIG.majorInterval * axisUnits[axisUnitRef.current].magnitude, axisStartRef.current - axisUnits[axisUnitRef.current].magnitude * 5) + ? Math.min( + fullEnd + AXIS_CONFIG.majorInterval * axisUnits[axisUnitRef.current].magnitude, + axisStartRef.current + axisUnits[axisUnitRef.current].magnitude * 5 + ) + : Math.max( + fullStart - AXIS_CONFIG.majorInterval * axisUnits[axisUnitRef.current].magnitude, + axisStartRef.current - axisUnits[axisUnitRef.current].magnitude * 5 + ) axisStartRef.current = newStart timelineWrapperRef.current.style.setProperty('--axis-start', newStart.toString()) @@ -232,39 +249,48 @@ const TimelineBar: FC<{ selectedId: string | undefined setSelectedId: (id: string | undefined) => void }> = ({ items, selectedId, setSelectedId }) => { + const start = items.reduce((acc, item) => Math.min(acc, item.start), Number.MAX_SAFE_INTEGER) + const end = items.reduce((acc, item) => Math.max(acc, item.end), Number.MIN_SAFE_INTEGER) + const status = items.length === 1 ? items[0].status : 'UNKNOWN' - const start = items.reduce((acc, item) => Math.min(acc, item.start) , Number.MAX_SAFE_INTEGER) - const end = items.reduce((acc, item) => Math.max(acc, item.end) , Number.MIN_SAFE_INTEGER) - const status = items.length === 1 ? items[0].status: 'UNKNOWN' - - return items.length && ( - - - - - - Tooltip Arrow - - - - {items.map(item => { - return + return ( + items.length && ( + + + + + + Tooltip Arrow + + + + {items.map((item) => { + return ( + + ) })} - - + + + ) ) } @@ -314,10 +340,10 @@ function formatTime(time: number): string { } function bucketItems(items: TimelineItem[], minDuration: number) { - const result: number[][] = []; + const result: number[][] = [] let i = 0 - while(i < items.length) { + while (i < items.length) { const bucket: number[] = [] let bucketDuration = 0 @@ -325,14 +351,13 @@ function bucketItems(items: TimelineItem[], minDuration: number) { bucket.push(i) bucketDuration += items[i].end - items[i].start + 1 i++ - } while(i < items.length && bucketDuration < minDuration) + } while (i < items.length && bucketDuration < minDuration) - - if(bucketDuration >= minDuration) { + if (bucketDuration >= minDuration) { result.push(bucket) } else { // Try merging with left - if(result.length > 0) { + if (result.length > 0) { result[result.length - 1].push(...bucket) } else { // No adjacent bucket exist @@ -341,5 +366,5 @@ function bucketItems(items: TimelineItem[], minDuration: number) { } } - return result; -} \ No newline at end of file + return result +} diff --git a/src/hooks/useTimelineData.ts b/src/hooks/useTimelineData.ts index 7a522703..ada20d62 100644 --- a/src/hooks/useTimelineData.ts +++ b/src/hooks/useTimelineData.ts @@ -76,7 +76,6 @@ export function useTimelineData(): TimelineData { const groupId = testCaseStarted.workerId ?? UNASSIGNED_GROUP_ID - const item = { id: testCaseStarted.id, groupId, @@ -90,18 +89,18 @@ export function useTimelineData(): TimelineData { testCaseStarted, } - if(groupMap[groupId]) { + if (groupMap[groupId]) { groupMap[groupId].items.push(item) } else { - groupMap[groupId] = {id: groupId, label: describeGroup(groupId), items: [item]} + groupMap[groupId] = { id: groupId, label: describeGroup(groupId), items: [item] } } } - for(const grp of Object.values(groupMap)) { + for (const grp of Object.values(groupMap)) { grp.items.sort((a, b) => a.start - b.start || a.end - b.end) } - const groups: TimelineGroup[] = Object.values(groupMap); + const groups: TimelineGroup[] = Object.values(groupMap) groups.sort((a, b) => compareGroupIds(a.id, b.id)) return { groups, fullStart, fullEnd } From 7b5de328fc7e753d976105d0666230bedc30f9d5 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Wed, 5 Aug 2026 00:54:01 +0500 Subject: [PATCH 23/26] Added useMemo for selectedItem and cleanup --- src/components/app/Timeline.tsx | 84 +++++++++++++++++++++------------ src/hooks/useTimelineData.ts | 2 +- 2 files changed, 56 insertions(+), 30 deletions(-) diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index 7ec50752..425d2e86 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -11,7 +11,7 @@ import { Tags } from '../gherkin/Tags.js' import { TestCaseOutcome } from '../results/index.js' import styles from './Timeline.module.scss' -type unit = { +type Unit = { label: string magnitude: number } @@ -21,9 +21,10 @@ const pxPerUnit = 2 const AXIS_CONFIG = { minorInterval: 5, // Minor tick every N × magnitude ms majorInterval: 50, // Major tick every N × magnitude ms (must be a multiple of minorInterval) + PANNING_SPEED: 5, } -const axisUnits: unit[] = [ +const axisUnits: Unit[] = [ { label: `${1 * AXIS_CONFIG.minorInterval} ms`, magnitude: 1 }, { label: `${10 * AXIS_CONFIG.minorInterval} ms`, magnitude: 10 }, { label: `${50 * AXIS_CONFIG.minorInterval} ms`, magnitude: 50 }, @@ -36,6 +37,11 @@ const axisUnits: unit[] = [ { label: `${10 * AXIS_CONFIG.minorInterval} min`, magnitude: 10 * 60 * 1000 }, { label: `${30 * AXIS_CONFIG.minorInterval} min`, magnitude: 30 * 60 * 1000 }, { label: `${1 * AXIS_CONFIG.minorInterval} hr`, magnitude: 1 * 60 * 60 * 1000 }, + { label: `${5 * AXIS_CONFIG.minorInterval} hr`, magnitude: 5 * 60 * 60 * 1000 }, + { label: `${10 * AXIS_CONFIG.minorInterval} hr`, magnitude: 10 * 60 * 60 * 1000 }, + { label: `${15 * AXIS_CONFIG.minorInterval} hr`, magnitude: 15 * 60 * 60 * 1000 }, + { label: `${20 * AXIS_CONFIG.minorInterval} hr`, magnitude: 20 * 60 * 60 * 1000 }, + { label: `${24 * AXIS_CONFIG.minorInterval} hr`, magnitude: 24 * 60 * 60 * 1000 }, ] export const Timeline: FC = () => { const { groups, fullStart, fullEnd } = useTimelineData() @@ -46,16 +52,28 @@ export const Timeline: FC = () => { const axisStartRef = useRef(fullStart) const axisUnitRef = useRef(0) - let selectedItem: TimelineItem | null = null + const selectedItem = useMemo(() => { + if (!selectedId) { + return null + } + + for (const grp of groups) { + const found = grp.items.find(item => item.id === selectedId); + if (found) { + return found + } + } + + return null; +}, [groups, selectedId]); useEffect(() => { if (timelineWrapperRef.current) { - console.log(`${fullStart} loaded`) timelineWrapperRef.current.style.setProperty( '--magnitude', axisUnits[axisUnitRef.current].magnitude.toString() ) - timelineWrapperRef.current.style.setProperty('--axis-start', axisStartRef.current.toString()) + timelineWrapperRef.current.style.setProperty('--axis-start', fullStart.toString()) } }, [fullStart]) @@ -78,12 +96,8 @@ export const Timeline: FC = () => {
{grp.label}
- {bucketItems(grp.items, axisUnits[axisUnitRef.current ?? 0].magnitude).map( + {bucketItems(grp.items, axisUnits[axisUnitRef.current].magnitude).map( (itemIds) => { - // selectedItem = selectedId === itemIds.id ? item: selectedItem - itemIds.forEach((id) => { - selectedItem = grp.items[id].id === selectedId ? grp.items[id] : selectedItem - }) return ( { + axisStartRef.current = Math.min(fullEnd + AXIS_CONFIG.majorInterval * magnitude, axisStartRef.current) + axisStartRef.current = Math.max(fullStart - AXIS_CONFIG.majorInterval * magnitude, axisStartRef.current) + } + const handleAxisZoom = useDebouncedCallback((deltaY: number) => { if (!timelineWrapperRef?.current) { return @@ -145,11 +164,18 @@ const TimelineAxis: FC<{ ? Math.min(axisUnits.length - 1, axisUnitRef.current + 1) : Math.max(0, axisUnitRef.current - 1) - axisUnitRef.current = newIndex - timelineWrapperRef.current.style.setProperty( - '--magnitude', - axisUnits[newIndex].magnitude.toString() - ) + + axisUnitRef.current = newIndex + timelineWrapperRef.current.style.setProperty( + '--magnitude', + axisUnits[newIndex].magnitude.toString() + ) + + // Check axis start for new zoom level + checkAxisStartBounds(axisUnits[newIndex].magnitude) + timelineWrapperRef.current.style.setProperty('--axis-start', axisStartRef.current.toString()) + + setCurrentUnitIndex(newIndex) }, 100) @@ -178,16 +204,15 @@ const TimelineAxis: FC<{ const newStart = deltaX < 0 - ? Math.min( - fullEnd + AXIS_CONFIG.majorInterval * axisUnits[axisUnitRef.current].magnitude, - axisStartRef.current + axisUnits[axisUnitRef.current].magnitude * 5 - ) - : Math.max( - fullStart - AXIS_CONFIG.majorInterval * axisUnits[axisUnitRef.current].magnitude, - axisStartRef.current - axisUnits[axisUnitRef.current].magnitude * 5 - ) + ? + axisStartRef.current + axisUnits[axisUnitRef.current].magnitude * AXIS_CONFIG.PANNING_SPEED + : + axisStartRef.current - axisUnits[axisUnitRef.current].magnitude * AXIS_CONFIG.PANNING_SPEED + axisStartRef.current = newStart + checkAxisStartBounds(axisUnits[axisUnitRef.current].magnitude) + timelineWrapperRef.current.style.setProperty('--axis-start', newStart.toString()) } @@ -211,7 +236,7 @@ const TimelineAxis: FC<{ return (
- {axisUnits[currentUnitIndex].label} + 1 Minor Tick = {axisUnits[currentUnitIndex].label}
@@ -278,7 +304,7 @@ const TimelineBar: FC<{
) @@ -149,8 +147,14 @@ const TimelineAxis: FC<{ }, [axisStartRef, fullStart, axisUnitRef, setCurrentUnitIndex]) const checkAxisStartBounds = (magnitude: number) => { - axisStartRef.current = Math.min(fullEnd + AXIS_CONFIG.majorInterval * magnitude, axisStartRef.current) - axisStartRef.current = Math.max(fullStart - AXIS_CONFIG.majorInterval * magnitude, axisStartRef.current) + axisStartRef.current = Math.min( + fullEnd + AXIS_CONFIG.majorInterval * magnitude, + axisStartRef.current + ) + axisStartRef.current = Math.max( + fullStart - AXIS_CONFIG.majorInterval * magnitude, + axisStartRef.current + ) } const handleAxisZoom = useDebouncedCallback((deltaY: number) => { @@ -164,17 +168,15 @@ const TimelineAxis: FC<{ ? Math.min(axisUnits.length - 1, axisUnitRef.current + 1) : Math.max(0, axisUnitRef.current - 1) - - axisUnitRef.current = newIndex - timelineWrapperRef.current.style.setProperty( - '--magnitude', - axisUnits[newIndex].magnitude.toString() - ) - - // Check axis start for new zoom level - checkAxisStartBounds(axisUnits[newIndex].magnitude) - timelineWrapperRef.current.style.setProperty('--axis-start', axisStartRef.current.toString()) - + axisUnitRef.current = newIndex + timelineWrapperRef.current.style.setProperty( + '--magnitude', + axisUnits[newIndex].magnitude.toString() + ) + + // Check axis start for new zoom level + checkAxisStartBounds(axisUnits[newIndex].magnitude) + timelineWrapperRef.current.style.setProperty('--axis-start', axisStartRef.current.toString()) setCurrentUnitIndex(newIndex) }, 100) @@ -204,11 +206,10 @@ const TimelineAxis: FC<{ const newStart = deltaX < 0 - ? - axisStartRef.current + axisUnits[axisUnitRef.current].magnitude * AXIS_CONFIG.PANNING_SPEED - : - axisStartRef.current - axisUnits[axisUnitRef.current].magnitude * AXIS_CONFIG.PANNING_SPEED - + ? axisStartRef.current + + axisUnits[axisUnitRef.current].magnitude * AXIS_CONFIG.PANNING_SPEED + : axisStartRef.current - + axisUnits[axisUnitRef.current].magnitude * AXIS_CONFIG.PANNING_SPEED axisStartRef.current = newStart checkAxisStartBounds(axisUnits[axisUnitRef.current].magnitude) @@ -258,7 +259,7 @@ const TimelineAxis: FC<{ key={time} className={isMajor ? styles.majorTick : styles.minorTick} style={{ - transform: `translateX(calc(((${time} - var(--axis-start)) / var(--magnitude)) *${pxPerUnit} * 1px))` + transform: `translateX(calc(((${time} - var(--axis-start)) / var(--magnitude)) *${pxPerUnit} * 1px))`, }} > {isMajor && {formatTime(time)}} @@ -287,7 +288,7 @@ const TimelineBar: FC<{ style={{ width: `calc( ( (${end - start + 1}) / var(--magnitude)) *${pxPerUnit} * 1px)`, // marginLeft: `calc(((${start} - var(--axis-start)) / var(--magnitude)) *${pxPerUnit} * 1px)`, - transform: `translateX(calc(((${start} - var(--axis-start)) / var(--magnitude)) *${pxPerUnit} * 1px))` + transform: `translateX(calc(((${start} - var(--axis-start)) / var(--magnitude)) *${pxPerUnit} * 1px))`, }} data-status={status} onClick={() => setSelectedId(items.length === 1 ? items[0].id : undefined)} @@ -359,7 +360,7 @@ const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item, ) } -function formatTime (time: number): string { +function formatTime(time: number): string { const d = new Date(time) const formattedTime = `${d.getHours()}:${d.getMinutes()}:${d.getSeconds()}:${d.getMilliseconds()}` return formattedTime From 263cd85fadee770c3aa33dd777b9cc53fe42b9c0 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Wed, 5 Aug 2026 01:35:37 +0500 Subject: [PATCH 25/26] Fixed axis tick and item detail UI --- src/components/app/Timeline.module.scss | 2 ++ src/components/app/Timeline.tsx | 9 ++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/app/Timeline.module.scss b/src/components/app/Timeline.module.scss index dd4bf825..88f2a9be 100644 --- a/src/components/app/Timeline.module.scss +++ b/src/components/app/Timeline.module.scss @@ -126,6 +126,8 @@ user-select: none; border: none; background-color: inherit; + padding: 0; + margin: 0; } .axisRuler:active { diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index 69ece0f2..4264e6e1 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -287,7 +287,6 @@ const TimelineBar: FC<{ className={`${styles.timelineBar} ${items.some((item) => item.id === selectedId) ? styles.selected : ''}`} style={{ width: `calc( ( (${end - start + 1}) / var(--magnitude)) *${pxPerUnit} * 1px)`, - // marginLeft: `calc(((${start} - var(--axis-start)) / var(--magnitude)) *${pxPerUnit} * 1px)`, transform: `translateX(calc(((${start} - var(--axis-start)) / var(--magnitude)) *${pxPerUnit} * 1px))`, }} data-status={status} @@ -343,10 +342,10 @@ const TimelineDetail: FC<{ item: TimelineItem; onClose: () => void }> = ({ item,
{formatTime(item.start)}
-
-
End
-
{formatTime(item.end)}
-
+
End
+
{formatTime(item.end)}
+
+
Duration
{formatExecutionDuration(new Date(item.start), new Date(item.end))}
From 2b8a4a0139251150260e42ca91f0f10626249b84 Mon Sep 17 00:00:00 2001 From: Muhammad Talha Date: Wed, 5 Aug 2026 01:38:21 +0500 Subject: [PATCH 26/26] Removed tooltip overlay arrow --- src/components/app/Timeline.tsx | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/app/Timeline.tsx b/src/components/app/Timeline.tsx index 4264e6e1..ec6a1cbd 100644 --- a/src/components/app/Timeline.tsx +++ b/src/components/app/Timeline.tsx @@ -1,7 +1,7 @@ import { faXmark } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { type FC, useEffect, useMemo, useRef, useState } from 'react' -import { Button, OverlayArrow, Tooltip, TooltipTrigger } from 'react-aria-components' +import { Button, Tooltip, TooltipTrigger } from 'react-aria-components' import { useDebouncedCallback } from 'use-debounce' import { formatExecutionDuration } from '../../formatExecutionDuration.js' import { type TimelineItem, useTimelineData } from '../../hooks/useTimelineData.js' @@ -293,12 +293,6 @@ const TimelineBar: FC<{ onClick={() => setSelectedId(items.length === 1 ? items[0].id : undefined)} > - - - Tooltip Arrow - - - {items.map((item) => { return (