• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

electron / fiddle / 22057459228

16 Feb 2026 09:37AM UTC coverage: 79.622% (+0.08%) from 79.539%
22057459228

Pull #1854

github

web-flow
Merge e6a8132f2 into 1ec6283c0
Pull Request #1854: feat: rework local builds

1617 of 1764 branches covered (91.67%)

Branch coverage included in aggregate %.

166 of 182 new or added lines in 6 files covered. (91.21%)

1 existing line in 1 file now uncovered.

9304 of 11952 relevant lines covered (77.84%)

32.38 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

95.21
/src/renderer/components/version-select.tsx
1
import * as React from 'react';
1✔
2

3
import {
1✔
4
  Button,
5
  ButtonGroupProps,
6
  ContextMenu,
7
  IconName,
8
  Intent,
9
  Menu,
10
  MenuItem,
11
} from '@blueprintjs/core';
12
import { Tooltip2 } from '@blueprintjs/popover2';
1✔
13
import {
1✔
14
  ItemListPredicate,
15
  ItemListRenderer,
16
  ItemRenderer,
17
  Select,
18
} from '@blueprintjs/select';
19
import { observer } from 'mobx-react';
1✔
20
import { FixedSizeList, ListChildComponentProps } from 'react-window';
1✔
21
import semver from 'semver';
1✔
22

23
import { InstallState, RunnableVersion, VersionSource } from '../../interfaces';
1✔
24
import { AppState } from '../state';
25
import { disableDownload } from '../utils/disable-download';
1✔
26
import { highlightText } from '../utils/highlight-text';
1✔
27

28
/**
29
 * Returns the display text for a version item.
30
 * For local builds, shows the custom name; for remote, shows the version string.
31
 */
32
export function getItemDisplayText(item: RunnableVersion): string {
1✔
33
  if (item.source === VersionSource.local) {
43✔
34
    return item.name || 'Local Build';
3✔
35
  }
3✔
36
  return item.version;
40✔
37
}
40✔
38

39
const ElectronVersionSelect = Select.ofType<RunnableVersion>();
1✔
40

41
/**
42
 * Represents either a real version item or a section header in the list.
43
 */
44
type ListEntry =
45
  | { type: 'header'; label: string }
46
  | { type: 'item'; item: RunnableVersion; originalIndex: number };
47

48
const HEADER_HEIGHT = 28;
1✔
49
const ITEM_HEIGHT = 30;
1✔
50

51
const FixedSizeListItem = ({ index, data, style }: ListChildComponentProps) => {
1✔
52
  const { entries, renderItem } = data;
40✔
53
  const entry: ListEntry = entries[index];
40✔
54

55
  if (entry.type === 'header') {
40✔
56
    return (
5✔
57
      <div
5✔
58
        style={{
5✔
59
          ...style,
5✔
60
          padding: '4px 8px',
5✔
61
          fontSize: '11px',
5✔
62
          fontWeight: 600,
5✔
63
          textTransform: 'uppercase',
5✔
64
          color: 'var(--text-muted, #999)',
5✔
65
          letterSpacing: '0.5px',
5✔
66
          borderBottom: '1px solid var(--divider, #333)',
5✔
67
          display: 'flex',
5✔
68
          alignItems: 'center',
5✔
69
          pointerEvents: 'none',
5✔
70
        }}
5✔
71
      >
72
        {entry.label}
5✔
73
      </div>
5✔
74
    );
75
  }
5✔
76

77
  const renderedItem = renderItem(entry.item, entry.originalIndex);
35✔
78
  return <div style={style}>{renderedItem}</div>;
35✔
79
};
35✔
80

81
/**
82
 * Builds a list of entries with section headers separating local and remote versions.
83
 */
84
function buildListEntries(filteredItems: RunnableVersion[]): ListEntry[] {
4✔
85
  const locals = filteredItems.filter((v) => v.source === VersionSource.local);
4✔
86
  const remotes = filteredItems.filter((v) => v.source !== VersionSource.local);
4✔
87

88
  const entries: ListEntry[] = [];
4✔
89

90
  if (locals.length > 0) {
4!
NEW
91
    entries.push({ type: 'header', label: 'Local Builds' });
×
NEW
92
    locals.forEach((item, i) =>
×
NEW
93
      entries.push({ type: 'item', item, originalIndex: i }),
×
NEW
94
    );
×
NEW
95
  }
×
96

97
  if (remotes.length > 0) {
4✔
98
    entries.push({ type: 'header', label: 'Releases' });
4✔
99
    remotes.forEach((item, i) =>
4✔
100
      entries.push({
28✔
101
        type: 'item',
28✔
102
        item,
28✔
103
        originalIndex: locals.length + i,
28✔
104
      }),
28✔
105
    );
4✔
106
  }
4✔
107

108
  return entries;
4✔
109
}
4✔
110

111
const itemListRenderer: ItemListRenderer<RunnableVersion> = ({
1✔
112
  filteredItems,
4✔
113
  renderItem,
4✔
114
  itemsParentRef,
4✔
115
}) => {
4✔
116
  const InnerElement = React.forwardRef((props, ref: React.Ref<Menu>) => {
4✔
117
    return <Menu ref={ref} ulRef={itemsParentRef} {...props} />;
5✔
118
  });
4✔
119
  InnerElement.displayName = 'Menu';
4✔
120

121
  const entries = buildListEntries(filteredItems);
4✔
122

123
  return (
4✔
124
    <FixedSizeList
4✔
125
      innerElementType={InnerElement}
4✔
126
      height={300}
4✔
127
      width={400}
4✔
128
      itemCount={entries.length}
4✔
129
      itemSize={ITEM_HEIGHT}
4✔
130
      itemData={{ renderItem, entries }}
4✔
131
    >
132
      {FixedSizeListItem}
4✔
133
    </FixedSizeList>
4✔
134
  );
135
};
4✔
136

137
/**
138
 * Helper method: Returns the <Select /> label for an Electron
139
 * version.
140
 */
141
export function getItemLabel({ source, state }: RunnableVersion): string {
1✔
142
  // If a version is local, show its availability state.
143
  if (source === VersionSource.local) {
43✔
144
    return state === InstallState.missing ? 'Unavailable' : 'Local Build';
3✔
145
  }
3✔
146

147
  const installStateLabels: Record<InstallState, string> = {
40✔
148
    missing: 'Not Downloaded',
40✔
149
    downloading: 'Downloading',
40✔
150
    downloaded: 'Downloaded',
40✔
151
    installing: 'Downloaded',
40✔
152
    installed: 'Downloaded',
40✔
153
  } as const;
40✔
154
  return installStateLabels[state] || '';
43!
155
}
43✔
156

157
/**
158
 * Helper method: Returns the <Select /> icon for an Electron
159
 * version.
160
 */
161
export function getItemIcon({ source, state }: RunnableVersion): IconName {
1✔
162
  // If a version is local, either it's there or it's not.
163
  if (source === VersionSource.local) {
44✔
164
    return state === InstallState.missing ? 'issue' : 'saved';
1!
165
  }
1✔
166

167
  const installStateIcons: Record<InstallState, IconName> = {
43✔
168
    missing: 'cloud',
43✔
169
    downloading: 'cloud-download',
43✔
170
    downloaded: 'compressed',
43✔
171
    installing: 'compressed',
43✔
172
    installed: 'saved',
43✔
173
  } as const;
43✔
174

175
  return installStateIcons[state] || '';
44!
176
}
44✔
177

178
/**
179
 * Helper method: Returns the <Select /> predicate for an Electron
180
 * version.
181
 *
182
 * Sorts by index of the chosen query.
183
 * For example, if we take the following versions:
184
 * [3.0.0, 14.3.0, 13.2.0, 12.0.0-nightly.20210301, 12.0.0-beta.3]
185
 * and a search query of '3', this method would sort them into:
186
 * [3.0.0, 13.2.0, 14.3.0, 12.0.0-beta.3, 12.0.0-nightly.20210301]
187
 */
188
export const filterItems: ItemListPredicate<RunnableVersion> = (
1✔
189
  query,
6✔
190
  versions,
6✔
191
) => {
6✔
192
  if (query === '') return versions;
6✔
193

194
  const q = query.toLowerCase();
4✔
195

196
  return versions
4✔
197
    .map((version: RunnableVersion) => {
4✔
198
      const lowercase = version.version.toLowerCase();
20✔
199
      // For local versions, also search by name
200
      const nameMatch =
20✔
201
        version.source === VersionSource.local && version.name
20✔
202
          ? version.name.toLowerCase().indexOf(q)
2✔
203
          : -1;
18✔
204
      const versionIndex = lowercase.indexOf(q);
20✔
205
      // Use best match (name or version string)
206
      const index = nameMatch !== -1 ? nameMatch : versionIndex;
20✔
207
      return {
20✔
208
        index,
20✔
209
        coerced: semver.coerce(lowercase),
20✔
210
        version,
20✔
211
      };
20✔
212
    })
4✔
213
    .filter((item) => item.index !== -1)
4✔
214
    .sort((a, b) => {
4✔
215
      // Local versions always sort first
216
      const aLocal = a.version.source === VersionSource.local;
15✔
217
      const bLocal = b.version.source === VersionSource.local;
15✔
218
      if (aLocal && !bLocal) return -1;
15!
219
      if (!aLocal && bLocal) return 1;
15!
220

221
      // If the user is searching for e.g. 'nightly' we
222
      // want to sort nightlies by descending major version.
223
      if (isNaN(+q)) {
15✔
224
        if (a.coerced && b.coerced) {
7✔
225
          return semver.rcompare(a.coerced, b.coerced);
7✔
226
        }
7✔
227
      }
7✔
228
      return a.index - b.index;
8✔
229
    })
4✔
230
    .map((item) => item.version);
4✔
231
};
4✔
232

233
/**
234
 * Renders a context menu to copy the current Electron version.
235
 *
236
 * @param version - the Electron version number to copy.
237
 */
238
export const renderVersionContextMenu = (
1✔
239
  e: React.MouseEvent<HTMLButtonElement>,
1✔
240
  version: string,
1✔
241
) => {
1✔
242
  e.preventDefault();
1✔
243

244
  ContextMenu.show(
1✔
245
    <Menu>
1✔
246
      <MenuItem
1✔
247
        text="Copy Version Number"
1✔
248
        onClick={() => {
1✔
249
          navigator.clipboard.writeText(version);
1✔
250
        }}
1✔
251
      />
1✔
252
    </Menu>,
1✔
253
    { left: e.clientX, top: e.clientY },
1✔
254
  );
1✔
255
};
1✔
256

257
/**
258
 * Helper method: Returns the <Select /> <MenuItem /> for Electron
259
 * versions.
260
 */
261
export const renderItem: ItemRenderer<RunnableVersion> = (
1✔
262
  item,
39✔
263
  { handleClick, modifiers, query },
39✔
264
) => {
39✔
265
  if (!modifiers.matchesPredicate) {
39✔
266
    return null;
1✔
267
  }
1✔
268

269
  const displayText = getItemDisplayText(item);
38✔
270

271
  if (disableDownload(item.version) && item.source !== VersionSource.local) {
39✔
272
    return (
1✔
273
      <Tooltip2
1✔
274
        className="disabled-menu-tooltip"
1✔
275
        modifiers={{
1✔
276
          flip: { enabled: false },
1✔
277
          preventOverflow: { enabled: false },
1✔
278
          hide: { enabled: false },
1✔
279
        }}
1✔
280
        position="bottom"
1✔
281
        intent={Intent.PRIMARY}
1✔
282
        content={`Version is not available on current OS`}
1✔
283
      >
284
        <MenuItem
1✔
285
          active={modifiers.active}
1✔
286
          data-testid="disabled-menu-item"
1✔
287
          disabled={true}
1✔
288
          text={highlightText(displayText, query)}
1✔
289
          key={item.version}
1✔
290
          label={getItemLabel(item)}
1✔
291
          icon={getItemIcon(item)}
1✔
292
        />
1✔
293
      </Tooltip2>
1✔
294
    );
295
  }
1✔
296

297
  return (
37✔
298
    <MenuItem
37✔
299
      active={modifiers.active}
37✔
300
      disabled={modifiers.disabled}
37✔
301
      text={highlightText(displayText, query)}
37✔
302
      key={item.version}
37✔
303
      onClick={handleClick}
37✔
304
      label={getItemLabel(item)}
37✔
305
      icon={getItemIcon(item)}
37✔
306
    />
37✔
307
  );
308
};
37✔
309

310
interface VersionSelectState {
311
  value: string;
312
}
313

314
interface VersionSelectProps {
315
  appState: AppState;
316
  disabled?: boolean;
317
  currentVersion: RunnableVersion;
318
  onVersionSelect: (version: RunnableVersion) => void;
319
  buttonGroupProps?: ButtonGroupProps;
320
  itemDisabled?:
321
    | keyof RunnableVersion
322
    | ((item: RunnableVersion, index: number) => boolean);
323
}
324

325
/**
326
 * A dropdown allowing the selection of Electron versions. The actual
327
 * download is managed in the state.
328
 */
329
export const VersionSelect = observer(
1✔
330
  class VersionSelect extends React.Component<
1✔
331
    VersionSelectProps,
332
    VersionSelectState
333
  > {
1✔
334
    public render() {
1✔
335
      const { currentVersion, itemDisabled } = this.props;
2✔
336
      const { version } = currentVersion;
2✔
337

338
      const buttonText = getItemDisplayText(currentVersion);
2✔
339
      const isLocal = currentVersion.source === VersionSource.local;
2✔
340

341
      return (
2✔
342
        <ElectronVersionSelect
2✔
343
          filterable={true}
2✔
344
          items={this.props.appState.versionsToShow}
2✔
345
          itemRenderer={renderItem}
2✔
346
          itemListPredicate={filterItems}
2✔
347
          itemListRenderer={itemListRenderer}
2✔
348
          itemDisabled={itemDisabled}
2✔
349
          onItemSelect={this.props.onVersionSelect}
2✔
350
          noResults={<MenuItem disabled={true} text="No results." />}
2✔
351
          disabled={!!this.props.disabled}
2✔
352
        >
353
          <Button
2✔
354
            id="version-chooser"
2✔
355
            text={buttonText}
2✔
356
            icon={getItemIcon(currentVersion)}
2✔
357
            data-local={isLocal ? 'true' : undefined}
2!
358
            onContextMenu={
2✔
359
              isLocal
2!
NEW
360
                ? undefined
×
361
                : (e: React.MouseEvent<HTMLButtonElement>) => {
2✔
362
                    renderVersionContextMenu(e, version);
1✔
363
                  }
1✔
364
            }
365
            disabled={!!this.props.disabled}
2✔
366
          />
2✔
367
        </ElectronVersionSelect>
2✔
368
      );
369
    }
2✔
370
  },
1✔
371
);
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc