(artifact: Artifact, duration: number)
| 101 | }; |
| 102 | |
| 103 | const urlSegments = (artifact: Artifact, duration: number): ReadonlyArray<UrlSegment> => { |
| 104 | if (basename(artifact.path) !== "session.mp4") return []; |
| 105 | const timeline = readTimeline(artifact); |
| 106 | const browserAnchor = timeline?.anchors.browser; |
| 107 | if (!timeline || browserAnchor === undefined) return []; |
| 108 | |
| 109 | const nav = [...(timeline.nav ?? [])] |
| 110 | .map((entry) => ({ |
| 111 | at: Math.max(0, (entry.at - browserAnchor) / 1000), |
| 112 | url: entry.url, |
| 113 | })) |
| 114 | .filter((entry) => entry.at <= duration) |
| 115 | .sort((a, b) => a.at - b.at); |
| 116 | |
| 117 | if (nav.length === 0) return [{ from: 0, to: duration, url: "about:blank" }]; |
| 118 | if ((nav[0]?.at ?? 0) <= 0.25) nav[0] = { ...nav[0], at: 0 }; |
| 119 | |
| 120 | const segments: UrlSegment[] = []; |
| 121 | let cursor = 0; |
| 122 | let currentUrl = "about:blank"; |
| 123 | for (const entry of nav) { |
| 124 | if (entry.at > cursor) segments.push({ from: cursor, to: entry.at, url: currentUrl }); |
| 125 | currentUrl = entry.url; |
| 126 | cursor = entry.at; |
| 127 | } |
| 128 | segments.push({ from: cursor, to: duration, url: currentUrl }); |
| 129 | |
| 130 | return segments |
| 131 | .filter((segment) => segment.to - segment.from > 0.01) |
| 132 | .reduce<UrlSegment[]>((merged, segment) => { |
| 133 | const previous = merged.at(-1); |
| 134 | if (previous?.url === segment.url && Math.abs(previous.to - segment.from) < 0.01) { |
| 135 | merged[merged.length - 1] = { ...previous, to: segment.to }; |
| 136 | } else { |
| 137 | merged.push(segment); |
| 138 | } |
| 139 | return merged; |
| 140 | }, []); |
| 141 | }; |
| 142 | |
| 143 | const htmlEscape = (value: string): string => |
| 144 | value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """); |
no test coverage detected