summaryrefslogtreecommitdiffhomepage
path: root/gui/test/e2e/utils.ts
blob: 082ccfcf3f28694c95781b38c0b0b38079ac4fcd (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
import { Locator, Page, _electron as electron, ElectronApplication } 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 = async (options: LaunchOptions): Promise<ElectronApplication> => {
  process.env.CI = 'e2e';
  return await electron.launch(options);
}

const currentRouteFactory = (app: ElectronApplication) => {
  return async () => {
    return await app.evaluate<string>(({ webContents }) => {
      return 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 = async (app: ElectronApplication): Promise<string> => {
  return await app.evaluate(({ ipcMain }) => {
    return 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
}