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

electron / fiddle / 19336107446

13 Nov 2025 03:10PM UTC coverage: 79.102% (-1.1%) from 80.223%
19336107446

Pull #1745

github

web-flow
Merge 2a1c60e8c into bc1b3083f
Pull Request #1745: feat: allow access to fiddle history

1526 of 1659 branches covered (91.98%)

Branch coverage included in aggregate %.

88 of 282 new or added lines in 8 files covered. (31.21%)

68 existing lines in 2 files now uncovered.

8951 of 11586 relevant lines covered (77.26%)

32.06 hits per line

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

83.62
/src/renderer/remote-loader.ts
1
import semver from 'semver';
1✔
2

3
import { ELECTRON_ORG, ELECTRON_REPO } from './constants';
1✔
4
import { AppState } from './state';
5
import { disableDownload } from './utils/disable-download';
1✔
6
import { isKnownFile, isSupportedFile } from './utils/editor-utils';
1✔
7
import { getOctokit } from './utils/octokit';
1✔
8
import { getReleaseChannel } from './versions';
1✔
9
import {
1✔
10
  EditorId,
11
  EditorValues,
12
  ElectronReleaseChannel,
13
  GenericDialogType,
14
  InstallState,
15
  PACKAGE_NAME,
16
  VersionSource,
17
} from '../interfaces';
18

19
// Define the GistRevision interface
20
interface GistRevision {
21
  sha: string;
22
  date: string;
23
  changes: {
24
    deletions: number;
25
    additions: number;
26
    total: number;
27
  };
28
}
29

30
export class RemoteLoader {
1✔
31
  constructor(private readonly appState: AppState) {
51✔
32
    for (const name of [
51✔
33
      'fetchExampleAndLoad',
51✔
34
      'fetchGistAndLoad',
51✔
35
      'handleLoadingFailed',
51✔
36
      'handleLoadingSuccess',
51✔
37
      'loadFiddleFromElectronExample',
51✔
38
      'loadFiddleFromGist',
51✔
39
      'setElectronVersion',
51✔
40
      'verifyReleaseChannelEnabled',
51✔
41
      'verifyRemoteLoad',
51✔
42
    ] as const) {
51✔
43
      this[name] = this[name].bind(this) as any;
459✔
44
    }
459✔
45
  }
51✔
46

47
  public async loadFiddleFromElectronExample(exampleInfo: {
51✔
48
    path: string;
49
    tag: string;
50
  }) {
2✔
51
    console.log(`Loading fiddle from Electron example`, exampleInfo);
2✔
52
    const { path, tag } = exampleInfo;
2✔
53
    const prettyName = path.replace('docs/fiddles/', '');
2✔
54
    const ok = await this.verifyRemoteLoad(
2✔
55
      `'${prettyName}' example from the Electron docs for version ${tag}`,
2✔
56
    );
2✔
57
    if (!ok) return;
2✔
58

59
    this.fetchExampleAndLoad(tag, path);
1✔
60
  }
2✔
61

62
  public async loadFiddleFromGist(gistInfo: { id: string }) {
51✔
63
    const { id } = gistInfo;
2✔
64
    const ok = await this.verifyRemoteLoad(`gist`);
2✔
65
    if (!ok) return;
2✔
66

67
    this.fetchGistAndLoad(id);
1✔
68
  }
2✔
69

70
  public async fetchExampleAndLoad(
51✔
71
    tag: string,
3✔
72
    path: string,
3✔
73
  ): Promise<boolean> {
3✔
74
    try {
3✔
75
      const octo = await getOctokit(this.appState);
3✔
76
      const folder = await octo.repos.getContents({
3✔
77
        owner: ELECTRON_REPO,
3✔
78
        repo: ELECTRON_ORG,
3✔
79
        ref: tag,
3✔
80
        path,
3✔
81
      });
3✔
82

83
      const index = tag.search(/\d/);
2✔
84
      const version = tag.substring(index);
2✔
85

86
      if (!semver.valid(version)) {
3!
UNCOV
87
        throw new Error('Could not determine Electron version for example');
×
UNCOV
88
      }
✔
89

90
      const ok = await this.setElectronVersion(version);
2✔
91
      if (!ok) return false;
3✔
92

93
      const values = await window.ElectronFiddle.getTemplate(
2✔
94
        this.appState.version,
2✔
95
      );
2✔
96
      if (!Array.isArray(folder.data)) {
3✔
97
        throw new Error(
1✔
98
          'The example Fiddle tried to launch is not a valid Electron example',
1✔
99
        );
1✔
100
      }
1✔
101

102
      const loaders: Array<Promise<void>> = [];
1✔
103

104
      for (const child of folder.data) {
3✔
105
        if (!child.download_url) {
6!
UNCOV
106
          console.warn(`Could not find download_url for ${child.name}`);
×
UNCOV
107
          continue;
×
UNCOV
108
        }
×
109

110
        if (isSupportedFile(child.name)) {
6✔
111
          loaders.push(
5✔
112
            fetch(child.download_url)
5✔
113
              .then((r) => r.text())
5✔
114
              .then((t) => {
5✔
115
                values[child.name as EditorId] = t;
5✔
116
              }),
5✔
117
          );
5✔
118
        }
5✔
119
      }
6✔
120

121
      await Promise.all(loaders);
1✔
122

123
      return this.handleLoadingSuccess(values, '');
1✔
124
    } catch (error: any) {
3✔
125
      return this.handleLoadingFailed(error);
2✔
126
    }
2✔
127
  }
3✔
128

129
  public async getGistRevisions(gistId: string): Promise<GistRevision[]> {
51✔
NEW
130
    console.log('getGistRevisions!');
×
NEW
131
    try {
×
NEW
132
      const octo = await getOctokit(this.appState);
×
NEW
133
      const revisions = await octo.gists.listCommits({
×
NEW
134
        gist_id: gistId,
×
NEW
135
      });
×
136

NEW
137
      console.log('Gist revisions', revisions.data);
×
138

NEW
139
      return revisions.data.map((r) => ({
×
NEW
UNCOV
140
        sha: r.version,
×
NEW
UNCOV
141
        date: r.committed_at,
×
NEW
UNCOV
142
        changes: r.change_status,
×
NEW
143
      }));
×
NEW
144
    } catch (error: any) {
×
NEW
145
      this.handleLoadingFailed(error);
×
NEW
146
      return [];
×
NEW
UNCOV
147
    }
×
NEW
UNCOV
148
  }
×
149

150
  /**
151
   * Load a fiddle
152
   */
153
  public async fetchGistAndLoad(
51✔
154
    gistId: string,
11✔
155
    revision?: string,
11✔
156
  ): Promise<boolean> {
11✔
157
    try {
11✔
158
      const octo = await getOctokit(this.appState);
11✔
159
      const gist = revision
11!
NEW
UNCOV
160
        ? await octo.gists.getRevision({ gist_id: gistId, sha: revision })
×
161
        : await octo.gists.get({ gist_id: gistId });
11✔
162

163
      const values: EditorValues = {};
10✔
164

165
      for (const [id, data] of Object.entries(gist.data.files)) {
11✔
166
        const content = data.truncated
55✔
167
          ? await fetch(data.raw_url!).then((r) => r.text())
1✔
168
          : data.content!;
54✔
169

170
        if (id === PACKAGE_NAME) {
55✔
171
          const deps: Record<string, string> = {};
5✔
172
          try {
5✔
173
            const { dependencies, devDependencies } = JSON.parse(content);
5✔
174
            Object.assign(deps, dependencies, devDependencies);
5✔
175
          } catch (e) {
5✔
176
            throw new Error('Invalid JSON found in package.json');
1✔
177
          }
1✔
178

179
          // If the gist specifies an Electron version, we want to tell Fiddle to run
180
          // it with that version by default.
181
          const electronDeps = Object.keys(deps).filter((d) =>
4✔
182
            ['electron-nightly', 'electron'].includes(d),
6✔
183
          );
4✔
184
          for (const dep of electronDeps) {
4✔
185
            // Strip off semver range prefixes, e.g:
186
            // ^1.2.0 -> 1.2.0
187
            // ~2.3.4 -> 2.3.4
188
            const index = deps[dep].search(/\d/);
4✔
189
            const version = deps[dep].substring(index);
4✔
190

191
            if (
4✔
192
              !semver.valid(version) ||
4✔
193
              !(await window.ElectronFiddle.isReleasedMajor(
4✔
194
                semver.major(version),
4✔
195
              ))
4✔
196
            ) {
4✔
197
              await this.appState.showGenericDialog({
3✔
198
                label: `The Electron version (${version}) in this gist's package.json is invalid. Falling back to last used version.`,
3✔
199
                ok: 'Close',
3✔
200
                type: GenericDialogType.warning,
3✔
201
                wantsInput: false,
3✔
202
              });
3✔
203
            } else if (disableDownload(version)) {
4✔
UNCOV
204
              await this.appState.showGenericDialog({
×
UNCOV
205
                label: `This gist's Electron version (${version}) is not available on your current OS. Falling back to last used version.`,
×
UNCOV
206
                ok: 'Close',
×
UNCOV
207
                type: GenericDialogType.warning,
×
UNCOV
208
                wantsInput: false,
×
UNCOV
209
              });
×
210
            } else {
1✔
211
              this.setElectronVersion(version);
1✔
212
            }
1✔
213

214
            // We want to include all dependencies except Electron.
215
            delete deps[dep];
4✔
216
          }
4✔
217

218
          this.appState.modules = new Map(Object.entries(deps));
4✔
219
        }
4✔
220

221
        // JSON files are supported, but we don't want to add package.json
222
        // or the lockfile to the visible editor array.
223
        if ([PACKAGE_NAME, 'package-lock.json'].includes(id)) continue;
54✔
224

225
        if (!isSupportedFile(id)) continue;
50✔
226

227
        if (isKnownFile(id) || (await this.confirmAddFile(id))) {
55✔
228
          values[id] = content;
48✔
229
        }
48✔
230
      }
55✔
231

232
      // If no files were populated into values, the Fiddle did not
233
      // contain any supported files. Throw an error to let the user know.
234
      if (Object.keys(values).length === 0) {
11✔
235
        throw new Error(
1✔
236
          'This Gist did not contain any supported files. Supported files must have one of the following extensions: .cjs, .js, .mjs, .css, or .html.',
1✔
237
        );
1✔
238
      }
1✔
239

240
      return this.handleLoadingSuccess(values, gistId);
8✔
241
    } catch (error: any) {
11✔
242
      return this.handleLoadingFailed(error);
3✔
243
    }
3✔
244
  }
11✔
245

246
  public async setElectronVersion(version: string): Promise<boolean> {
51✔
247
    if (!this.appState.hasVersion(version)) {
4✔
248
      const versionToDownload = {
2✔
249
        source: VersionSource.remote,
2✔
250
        state: InstallState.missing,
2✔
251
        version,
2✔
252
      };
2✔
253

254
      try {
2✔
255
        this.appState.addNewVersions([versionToDownload]);
2✔
256
        await this.appState.downloadVersion(versionToDownload);
2✔
257
      } catch {
2!
UNCOV
258
        await this.appState.removeVersion(versionToDownload);
×
UNCOV
259
        this.handleLoadingFailed(
×
UNCOV
260
          new Error(`Failed to download Electron version ${version}`),
×
UNCOV
261
        );
×
UNCOV
262
        return false;
×
UNCOV
263
      }
×
264
    }
2✔
265

266
    // check if version is part of release channel
267
    const versionReleaseChannel: ElectronReleaseChannel =
4✔
268
      getReleaseChannel(version);
4✔
269

270
    if (!this.appState.channelsToShow.includes(versionReleaseChannel)) {
4✔
271
      const ok = await this.verifyReleaseChannelEnabled(versionReleaseChannel);
1✔
272
      if (!ok) return false;
1!
273

274
      this.appState.channelsToShow.push(versionReleaseChannel);
1✔
275
    }
1✔
276

277
    this.appState.setVersion(version);
4✔
278
    return true;
4✔
279
  }
4✔
280

281
  public confirmAddFile = (filename: string): Promise<boolean> => {
51✔
UNCOV
282
    return this.appState.showConfirmDialog({
×
UNCOV
283
      cancel: 'Skip',
×
UNCOV
284
      label: `Do you want to add "${filename}"?`,
×
UNCOV
285
      ok: 'Add',
×
UNCOV
286
    });
×
UNCOV
287
  };
×
288

289
  /**
290
   * Verifies from the user that we should be loading this fiddle.
291
   *
292
   * @param what - What are we loading from (gist, example, etc.)
293
   */
294
  public verifyRemoteLoad(what: string): Promise<boolean> {
51✔
295
    return this.appState.showConfirmDialog({
4✔
296
      label: `Are you sure you want to load this ${what}? Only load and run it if you trust the source.`,
4✔
297
      ok: 'Load',
4✔
298
    });
4✔
299
  }
4✔
300

301
  public verifyReleaseChannelEnabled(channel: string): Promise<boolean> {
51✔
302
    return this.appState.showConfirmDialog({
1✔
303
      label: `You're loading an example with a version of Electron with an unincluded release
1✔
304
              channel (${channel}). Do you want to enable the release channel to load the
1✔
305
              version of Electron from the example?`,
306
      ok: 'Enable',
1✔
307
    });
1✔
308
  }
1✔
309

310
  /**
311
   * Loading a fiddle from GitHub succeeded, let's move on.
312
   */
313
  private async handleLoadingSuccess(
51✔
314
    values: EditorValues,
9✔
315
    gistId: string,
9✔
316
  ): Promise<boolean> {
9✔
317
    await window.app.replaceFiddle(values, { gistId });
9✔
318
    return true;
9✔
319
  }
9✔
320

321
  /**
322
   * Loading a fiddle from GitHub failed - this method handles this case
323
   * gracefully.
324
   */
325
  private handleLoadingFailed(error: Error): false {
51✔
326
    const failedLabel = `Loading the fiddle failed: ${error.message}`;
5✔
327
    this.appState.showErrorDialog(
5✔
328
      this.appState.isOnline
5✔
329
        ? failedLabel
5!
UNCOV
330
        : `Your computer seems to be offline. ${failedLabel}`,
×
331
    );
5✔
332

333
    console.warn(`Loading Fiddle failed`, error);
5✔
334
    return false;
5✔
335
  }
5✔
336
}
51✔
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