2022-04-21 02:03:49 +00:00
|
|
|
import { once } from 'events';
|
|
|
|
import * as fs from 'fs';
|
|
|
|
import * as path from 'path';
|
|
|
|
|
|
|
|
import { Shell } from 'electron';
|
|
|
|
import {
|
|
|
|
_electron,
|
|
|
|
ConsoleMessage,
|
|
|
|
Dialog,
|
|
|
|
ElectronApplication,
|
|
|
|
Page,
|
|
|
|
} from 'playwright';
|
|
|
|
|
|
|
|
import { getTempDir } from './helpers/helpers';
|
|
|
|
import { NativefierOptions } from '../shared/src/options/model';
|
|
|
|
|
|
|
|
const INJECT_DIR = path.join(__dirname, '..', 'app', 'inject');
|
|
|
|
|
|
|
|
const log = console;
|
|
|
|
|
|
|
|
function sleep(milliseconds: number): Promise<void> {
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
setTimeout(resolve, milliseconds);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
describe('Application launch', () => {
|
|
|
|
jest.setTimeout(60000);
|
|
|
|
|
|
|
|
let app: ElectronApplication;
|
|
|
|
let appClosed = true;
|
|
|
|
|
|
|
|
const appMainJSPath = path.join(__dirname, '..', 'app', 'lib', 'main.js');
|
|
|
|
const DEFAULT_CONFIG: NativefierOptions = {
|
|
|
|
targetUrl: 'https://npmjs.com',
|
|
|
|
};
|
|
|
|
|
|
|
|
const logFileDir = getTempDir('playwright');
|
|
|
|
|
|
|
|
const metaOrAlt = process.platform === 'darwin' ? 'Meta' : 'Alt';
|
|
|
|
const metaOrCtrl = process.platform === 'darwin' ? 'Meta' : 'Control';
|
|
|
|
|
|
|
|
const spawnApp = async (
|
|
|
|
playwrightConfig: NativefierOptions = { ...DEFAULT_CONFIG },
|
|
|
|
awaitFirstWindow = true,
|
|
|
|
preventNavigation = false,
|
|
|
|
): Promise<Page | undefined> => {
|
|
|
|
const consoleListener = (consoleMessage: ConsoleMessage): void => {
|
|
|
|
const consoleMethods: Record<string, (...args: unknown[]) => unknown> = {
|
|
|
|
debug: log.debug.bind(console),
|
|
|
|
error: log.error.bind(console),
|
|
|
|
info: log.info.bind(console),
|
|
|
|
log: log.log.bind(console),
|
|
|
|
trace: log.trace.bind(console),
|
|
|
|
warn: log.warn.bind(console),
|
|
|
|
};
|
|
|
|
Promise.all(consoleMessage.args().map((x) => x.jsonValue()))
|
|
|
|
.then((args) => {
|
|
|
|
if (consoleMessage.type() in consoleMethods) {
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
|
|
|
|
consoleMethods[consoleMessage.type()]('window.console', args);
|
|
|
|
} else {
|
|
|
|
log.log('window.console', args);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(() => log.log('window.console', consoleMessage));
|
|
|
|
};
|
|
|
|
app = await _electron.launch({
|
|
|
|
args: [appMainJSPath],
|
|
|
|
env: {
|
|
|
|
LOG_FILE_DIR: logFileDir,
|
|
|
|
PLAYWRIGHT_TEST: '1',
|
|
|
|
PLAYWRIGHT_CONFIG: JSON.stringify(playwrightConfig),
|
|
|
|
USE_LOG_FILE: '1',
|
|
|
|
VERBOSE: '1',
|
|
|
|
},
|
2022-04-25 14:11:11 +00:00
|
|
|
timeout: 60000,
|
2022-04-21 02:03:49 +00:00
|
|
|
});
|
|
|
|
app.on('window', (page: Page) => {
|
|
|
|
page.on('console', consoleListener);
|
|
|
|
if (preventNavigation) {
|
|
|
|
// Prevent page navigation so we can have a reliable test
|
|
|
|
page
|
|
|
|
.route('*', (route): void => {
|
|
|
|
log.info(`Preventing route: ${route.request().url()}`);
|
|
|
|
route.abort().catch((error) => {
|
|
|
|
log.error('ERROR', error);
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.catch((error) => {
|
|
|
|
log.error('ERROR', error);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
app.on('close', () => (appClosed = true));
|
|
|
|
appClosed = false;
|
|
|
|
if (!awaitFirstWindow) {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
const window = await app.firstWindow();
|
|
|
|
// Wait for our initial page to finish loading, otherwise some tests will break
|
|
|
|
let waited = 0;
|
|
|
|
while (
|
|
|
|
window.url() === 'about:blank' &&
|
|
|
|
playwrightConfig.targetUrl !== 'about:blank' &&
|
|
|
|
waited < 2000
|
|
|
|
) {
|
|
|
|
waited += 100;
|
|
|
|
await sleep(100);
|
|
|
|
}
|
|
|
|
return window;
|
|
|
|
};
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
nukeInjects();
|
|
|
|
nukeLogs(logFileDir);
|
|
|
|
});
|
|
|
|
|
|
|
|
afterEach(async () => {
|
|
|
|
if (app && !appClosed) {
|
|
|
|
await app.close();
|
|
|
|
}
|
|
|
|
if (process.env.DEBUG) {
|
|
|
|
showLogs(logFileDir);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
test('shows an initial window', async () => {
|
|
|
|
const mainWindow = (await spawnApp()) as Page;
|
|
|
|
await mainWindow.waitForLoadState('domcontentloaded');
|
|
|
|
expect(app.windows()).toHaveLength(1);
|
|
|
|
expect(await mainWindow.title()).toBe('npm');
|
|
|
|
});
|
|
|
|
|
|
|
|
test('can inject some CSS', async () => {
|
|
|
|
const fuschia = 'rgb(255, 0, 255)';
|
|
|
|
createInject(
|
|
|
|
'inject.css',
|
|
|
|
`* { background-color: ${fuschia} !important; }`,
|
|
|
|
);
|
|
|
|
const mainWindow = (await spawnApp()) as Page;
|
|
|
|
await mainWindow.waitForLoadState('domcontentloaded');
|
|
|
|
const headerStyle = await mainWindow.$eval('header', (el) =>
|
|
|
|
window.getComputedStyle(el),
|
|
|
|
);
|
|
|
|
expect(headerStyle.backgroundColor).toBe(fuschia);
|
|
|
|
|
|
|
|
await mainWindow.click('#nav-products-link');
|
|
|
|
await mainWindow.waitForLoadState('domcontentloaded');
|
|
|
|
const headerStylePostNavigate = await mainWindow.$eval('header', (el) =>
|
|
|
|
window.getComputedStyle(el),
|
|
|
|
);
|
|
|
|
expect(headerStylePostNavigate.backgroundColor).toBe(fuschia);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('can inject some JS', async () => {
|
|
|
|
const alertMsg = 'hello world from inject';
|
|
|
|
createInject(
|
|
|
|
'inject.js',
|
|
|
|
`setTimeout(() => {alert("${alertMsg}"); }, 5000);`, // Buy ourselves 5 seconds to get the dialog handler setup
|
|
|
|
);
|
|
|
|
const mainWindow = (await spawnApp(
|
|
|
|
{ ...DEFAULT_CONFIG },
|
|
|
|
true,
|
|
|
|
true,
|
|
|
|
)) as Page;
|
|
|
|
const [dialogPromise] = (await once(
|
|
|
|
mainWindow,
|
|
|
|
'dialog',
|
|
|
|
)) as unknown as Promise<Dialog>[];
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
|
|
|
const dialog: Dialog = await dialogPromise;
|
|
|
|
await dialog.dismiss();
|
|
|
|
expect(dialog.message()).toBe(alertMsg);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('can open internal links', async () => {
|
|
|
|
const mainWindow = (await spawnApp()) as Page;
|
|
|
|
await mainWindow.waitForLoadState('domcontentloaded');
|
|
|
|
await mainWindow.click('#nav-products-link');
|
|
|
|
await mainWindow.waitForLoadState('domcontentloaded');
|
|
|
|
expect(app.windows()).toHaveLength(1);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('tries to open external links', async () => {
|
|
|
|
const mainWindow = (await spawnApp()) as Page;
|
|
|
|
await mainWindow.waitForLoadState('domcontentloaded');
|
|
|
|
|
|
|
|
// Install the mock first
|
|
|
|
await app.evaluate(({ shell }: { shell: Shell }) => {
|
|
|
|
// @ts-expect-error injecting into shell so that this promise
|
|
|
|
// can be accessed outside of this anonymous function's scope
|
|
|
|
// Not my favorite thing to do, but I could not find another way
|
|
|
|
process.openExternalPromise = new Promise((resolve) => {
|
|
|
|
shell.openExternal = async (url: string): Promise<void> => {
|
|
|
|
resolve(url);
|
|
|
|
return Promise.resolve();
|
|
|
|
};
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
// Click, but don't await it - Playwright waits for stuff that does not happen when Electron does openExternal.
|
|
|
|
mainWindow
|
|
|
|
.click('#footer > div:nth-child(2) > ul > li:nth-child(2) > a')
|
|
|
|
.catch((err: unknown) => {
|
|
|
|
expect(err).toBeUndefined();
|
|
|
|
});
|
|
|
|
|
|
|
|
// Go pull out our value returned by our hacky global promise
|
|
|
|
const openExternalUrl = await app.evaluate('process.openExternalPromise');
|
|
|
|
expect(openExternalUrl).not.toBe('https://www.npmjs.com/');
|
|
|
|
|
|
|
|
expect(openExternalUrl).not.toBe(DEFAULT_CONFIG.targetUrl);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Currently disabled. Playwright doesn't seem to support app keypress events for menu shortcuts.
|
|
|
|
// Will enable when https://github.com/microsoft/playwright/issues/8004 is resolved.
|
|
|
|
test.skip('keyboard shortcuts: zoom', async () => {
|
|
|
|
const mainWindow = (await spawnApp()) as Page;
|
|
|
|
await mainWindow.waitForLoadState('domcontentloaded');
|
|
|
|
|
|
|
|
const defaultZoom: number | undefined = await app.evaluate(
|
|
|
|
({ BrowserWindow }) =>
|
|
|
|
BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor,
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(defaultZoom).toBeDefined();
|
|
|
|
|
|
|
|
await mainWindow.keyboard.press(`${metaOrCtrl}+Equal`);
|
|
|
|
const postZoomIn = await app.evaluate(
|
|
|
|
({ BrowserWindow }): number | undefined =>
|
|
|
|
BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor,
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(postZoomIn).toBeGreaterThan(defaultZoom as number);
|
|
|
|
|
|
|
|
await mainWindow.keyboard.press(`${metaOrCtrl}+0`);
|
|
|
|
const postZoomReset = await app.evaluate(
|
|
|
|
({ BrowserWindow }): number | undefined =>
|
|
|
|
BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor,
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(postZoomReset).toEqual(defaultZoom);
|
|
|
|
|
|
|
|
await mainWindow.keyboard.press(`${metaOrCtrl}+Minus`);
|
|
|
|
const postZoomOut: number | undefined = await app.evaluate(
|
|
|
|
({ BrowserWindow }) =>
|
|
|
|
BrowserWindow.getFocusedWindow()?.webContents?.zoomFactor,
|
|
|
|
);
|
|
|
|
|
|
|
|
expect(postZoomOut).toBeLessThan(defaultZoom as number);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Currently disabled. Playwright doesn't seem to support app keypress events for menu shortcuts.
|
|
|
|
// Will enable when https://github.com/microsoft/playwright/issues/8004 is resolved.
|
|
|
|
test.skip('keyboard shortcuts: back and forward', async () => {
|
|
|
|
const mainWindow = (await spawnApp()) as Page;
|
|
|
|
await mainWindow.waitForLoadState('domcontentloaded');
|
|
|
|
|
|
|
|
await Promise.all([
|
|
|
|
mainWindow.click('#nav-products-link'),
|
|
|
|
mainWindow.waitForNavigation({ waitUntil: 'domcontentloaded' }),
|
|
|
|
]);
|
|
|
|
|
|
|
|
// Go back
|
|
|
|
// console.log(`${metaOrAlt}+ArrowLeft`);
|
|
|
|
await mainWindow.keyboard.press(`${metaOrAlt}+ArrowLeft`);
|
|
|
|
await mainWindow.waitForNavigation({ waitUntil: 'domcontentloaded' });
|
|
|
|
|
|
|
|
const backUrl = await mainWindow.evaluate(() => window.location.href);
|
|
|
|
|
|
|
|
expect(backUrl).toBe(DEFAULT_CONFIG.targetUrl);
|
|
|
|
|
|
|
|
// Go forward
|
|
|
|
// console.log(`${metaOrAlt}+ArrowRight`);
|
|
|
|
await mainWindow.keyboard.press(`${metaOrAlt}+ArrowRight`);
|
|
|
|
await mainWindow.waitForNavigation({ waitUntil: 'domcontentloaded' });
|
|
|
|
|
|
|
|
const forwardUrl = await mainWindow.evaluate(() => window.location.href);
|
|
|
|
|
|
|
|
expect(forwardUrl).not.toBe(DEFAULT_CONFIG.targetUrl);
|
|
|
|
});
|
|
|
|
|
|
|
|
test('no errors thrown in console', async () => {
|
|
|
|
await spawnApp({ ...DEFAULT_CONFIG }, false);
|
|
|
|
const mainWindow = await app.firstWindow();
|
|
|
|
mainWindow.addListener('console', (consoleMessage: ConsoleMessage) => {
|
|
|
|
try {
|
|
|
|
expect(consoleMessage.type()).not.toBe('error');
|
|
|
|
} catch {
|
|
|
|
// Do it this way so we'll see the whole message, not just
|
|
|
|
// expect('error').not.toBe('error')
|
|
|
|
// which isn't particularly useful
|
|
|
|
expect({
|
|
|
|
message: 'console.error called unexpectedly with',
|
|
|
|
consoleMessage,
|
|
|
|
}).toBeUndefined();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
// Give the app 5 seconds to spin up and ensure no errors happened
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
|
|
});
|
|
|
|
|
|
|
|
test('basic auth', async () => {
|
|
|
|
const mainWindow = (await spawnApp({
|
|
|
|
targetUrl: 'http://httpbin.org/basic-auth/foo/bar',
|
|
|
|
basicAuthUsername: 'foo',
|
|
|
|
basicAuthPassword: 'bar',
|
|
|
|
})) as Page;
|
|
|
|
await mainWindow.waitForLoadState('networkidle');
|
|
|
|
|
|
|
|
const documentText = await mainWindow.evaluate<string>(
|
|
|
|
'document.documentElement.innerText',
|
|
|
|
);
|
|
|
|
|
|
|
|
const documentJSON = JSON.parse(documentText) as {
|
|
|
|
authenticated: boolean;
|
|
|
|
user: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
expect(documentJSON).toEqual({
|
|
|
|
authenticated: true,
|
|
|
|
user: 'foo',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
test('basic auth without pre-providing', async () => {
|
|
|
|
const mainWindow = (await spawnApp({
|
|
|
|
targetUrl: 'http://httpbin.org/basic-auth/foo/bar',
|
|
|
|
})) as Page;
|
|
|
|
await mainWindow.waitForLoadState('load');
|
|
|
|
|
|
|
|
// Give the app a few seconds to open the login window
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
|
|
|
|
|
|
const appWindows = app.windows();
|
|
|
|
|
|
|
|
expect(appWindows).toHaveLength(2);
|
|
|
|
|
|
|
|
const loginWindow = appWindows.filter((x) => x !== mainWindow)[0];
|
|
|
|
|
|
|
|
await loginWindow.waitForLoadState('domcontentloaded');
|
|
|
|
|
|
|
|
const usernameField = await loginWindow.$('#username-input');
|
|
|
|
|
|
|
|
expect(usernameField).not.toBeNull();
|
|
|
|
|
|
|
|
const passwordField = await loginWindow.$('#password-input');
|
|
|
|
|
|
|
|
expect(passwordField).not.toBeNull();
|
|
|
|
|
|
|
|
const submitButton = await loginWindow.$('#submit-form-button');
|
|
|
|
|
|
|
|
expect(submitButton).not.toBeNull();
|
|
|
|
|
|
|
|
await usernameField?.fill('foo');
|
|
|
|
await passwordField?.fill('bar');
|
|
|
|
await submitButton?.click();
|
|
|
|
|
|
|
|
await mainWindow.waitForLoadState('networkidle');
|
|
|
|
|
|
|
|
const documentText = await mainWindow.evaluate<string>(
|
|
|
|
'document.documentElement.innerText',
|
|
|
|
);
|
|
|
|
|
|
|
|
const documentJSON = JSON.parse(documentText) as {
|
|
|
|
authenticated: boolean;
|
|
|
|
user: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
expect(documentJSON).toEqual({
|
|
|
|
authenticated: true,
|
|
|
|
user: 'foo',
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
function createInject(filename: string, contents: string): void {
|
|
|
|
fs.writeFileSync(path.join(INJECT_DIR, filename), contents);
|
|
|
|
}
|
|
|
|
|
|
|
|
function nukeInjects(): void {
|
|
|
|
if (!fs.existsSync(INJECT_DIR)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
const injected = fs
|
|
|
|
.readdirSync(INJECT_DIR)
|
|
|
|
.filter((x) => x !== '_placeholder');
|
|
|
|
injected.forEach((x) => fs.unlinkSync(path.join(INJECT_DIR, x)));
|
|
|
|
}
|
|
|
|
|
|
|
|
function nukeLogs(logFileDir: string): void {
|
|
|
|
const logs = fs.readdirSync(logFileDir).filter((x) => x.endsWith('.log'));
|
|
|
|
logs.forEach((x) => fs.unlinkSync(path.join(logFileDir, x)));
|
|
|
|
}
|
|
|
|
|
|
|
|
function showLogs(logFileDir: string): void {
|
|
|
|
const logs = fs.readdirSync(logFileDir).filter((x) => x.endsWith('.log'));
|
|
|
|
for (const logFile of logs) {
|
|
|
|
log.log(fs.readFileSync(path.join(logFileDir, logFile)).toString());
|
|
|
|
}
|
|
|
|
}
|