summaryrefslogtreecommitdiffhomepage
path: root/gui/test/e2e/utils.ts
blob: 97524f69bcda1c1d78df51a73c04ee136e764820 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
import fs from 'fs';
import { _electron as electron, ElectronApplication, Locator, Page } from 'playwright';

export interface StartAppResponse {
  app: ElectronApplication;
  page: Page;
  util: TestUtils;
}

export interface TestUtils {
  currentRoute: () => Promise<string>;
  waitForNavigation: (initiateNavigation?: () => Promise<void> | void) => Promise<string>;
  waitForNoTransition: () => Promise<void>;
}

interface History {
  entries: Array<{ pathname: string }>;
  index: number;
}

type LaunchOptions = NonNullable<Parameters<typeof electron.launch>[0]>;

export const startApp = async (options: LaunchOptions): Promise<StartAppResponse> => {
  const app = await launch(options);
  const page = await app.firstWindow();

  // Wait for initial navigation to finish
  await waitForNoTransition(page);

  page.on('pageerror', (error) => console.log(error));
  page.on('console', (msg) => console.log(msg.text()));

  const util: TestUtils = {
    currentRoute: currentRouteFactory(app),
    waitForNavigation: waitForNavigationFactory(app, page),
    waitForNoTransition: () => waitForNoTransition(page),
  };

  return { app, page, util };
};

export const launch = (options: LaunchOptions): Promise<ElectronApplication> => {
  process.env.CI = 'e2e';
  return electron.launch(options);
};

const currentRouteFactory = (app: ElectronApplication) => {
  return () =>
    app.evaluate<string>(({ webContents }) =>
      webContents
        .getAllWebContents()
        // Select window that isn't devtools
        .find((webContents) => webContents.getURL().startsWith('file://'))!
        .executeJavaScript('window.e2e.location'),
    );
};

const waitForNavigationFactory = (app: ElectronApplication, page: Page) => {
  // Wait for navigation animation to finish. A function can be provided that initiates the
  // navigation, e.g. clicks a button.
  return async (initiateNavigation?: () => Promise<void> | void) => {
    // Wait for route to change after optionally initiating the navigation.
    const [route] = await Promise.all([waitForNextRoute(app), initiateNavigation?.()]);

    // Wait for view corresponding to new route to appear
    await page.getByTestId(route).isVisible();
    await waitForNoTransition(page);

    return route;
  };
};

const waitForNoTransition = async (page: Page) => {
  // Wait until there's only one transitionContents
  let transitionContentsCount;
  do {
    if (transitionContentsCount !== undefined) {
      await new Promise((resolve) => setTimeout(resolve, 5));
    }

    try {
      transitionContentsCount = await page.getByTestId('transition-content').count();
    } catch {
      console.log('Transition content count failed');
      break;
    }
  } while (transitionContentsCount !== 1);
};

// Returns the route when it changes
const waitForNextRoute = (app: ElectronApplication): Promise<string> => {
  return app.evaluate(
    ({ ipcMain }) =>
      new Promise((resolve) => {
        ipcMain.once('navigation-setHistory', (_event, history: History) => {
          resolve(history.entries[history.index].pathname);
        });
      }),
  );
};

const getStyleProperty = (locator: Locator, property: string) => {
  return locator.evaluate(
    (el, { property }) => {
      return window.getComputedStyle(el).getPropertyValue(property);
    },
    { property },
  );
};

export const getColor = (locator: Locator) => {
  return getStyleProperty(locator, 'color');
};

export const getBackgroundColor = (locator: Locator) => {
  return getStyleProperty(locator, 'background-color');
};

export function anyOf(...values: string[]): RegExp {
  return new RegExp(values.map(escapeRegExp).join('|'));
}

export function escapeRegExp(regexp: string): string {
  return regexp.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}

export function fileExists(filePath: string): boolean {
  try {
    fs.accessSync(filePath);
    return true;
  } catch {
    return false;
  }
}