diff --git a/packages/insomnia-smoke-test/fixtures/pre-request-collection.yaml b/packages/insomnia-smoke-test/fixtures/pre-request-collection.yaml index 471b19371..070568c9c 100644 --- a/packages/insomnia-smoke-test/fixtures/pre-request-collection.yaml +++ b/packages/insomnia-smoke-test/fixtures/pre-request-collection.yaml @@ -1114,3 +1114,59 @@ resources: text: |- {} _type: request + - _id: req_89dade2ee9ee42fbb22d588783a9df17 + parentId: fld_01de564274824ecaad272330339ea6b2 + modified: 1636707449231 + created: 1636141014552 + url: http://127.0.0.1:4010/echo + name: infinite loop + description: "" + method: POST + parameters: [] + headers: + - name: 'Content-Type' + value: 'application/json' + authentication: {} + metaSortKey: -1636141014553 + isPrivate: false + settingStoreCookies: true + settingSendCookies: true + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingRebuildPath: true + settingFollowRedirects: global + preRequestScript: |- + while(true) {} + body: + mimeType: "application/json" + text: |- + {} + _type: request + - _id: req_89dade2ee9ee42fbb22d588783a9df18 + parentId: fld_01de564274824ecaad272330339ea6b2 + modified: 1636707449231 + created: 1636141014552 + url: http://127.0.0.1:4010/echo + name: simple log + description: "" + method: POST + parameters: [] + headers: + - name: 'Content-Type' + value: 'application/json' + authentication: {} + metaSortKey: -1636141014553 + isPrivate: false + settingStoreCookies: true + settingSendCookies: true + settingDisableRenderRequestBody: false + settingEncodeUrl: true + settingRebuildPath: true + settingFollowRedirects: global + preRequestScript: |- + console.log('back to normal'); + body: + mimeType: "application/json" + text: |- + {} + _type: request diff --git a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts index 5a16b160a..7278415ac 100644 --- a/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts +++ b/packages/insomnia-smoke-test/tests/smoke/pre-request-script-window.test.ts @@ -1,60 +1,98 @@ +import { expect } from '@playwright/test'; + import { loadFixture } from '../../playwright/paths'; import { test } from '../../playwright/test'; -test('can cancel pre-request script', async ({ app, page }) => { - test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); +test.describe('test hidden window handling', async () => { + test('can cancel pre-request script', async ({ app, page }) => { + test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); - const text = await loadFixture('pre-request-collection.yaml'); - await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); + const text = await loadFixture('pre-request-collection.yaml'); + await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - await page.getByRole('button', { name: 'Create in project' }).click(); - await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.locator('[data-test-id="import-from-clipboard"]').click(); - await page.getByRole('button', { name: 'Scan' }).click(); - await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); + await page.getByRole('button', { name: 'Create in project' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); + await page.getByRole('button', { name: 'Scan' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); - await page.getByRole('button', { name: 'Workspace actions menu button' }).click(); - await page.getByRole('menuitem', { name: 'Export' }).click(); - await page.getByRole('button', { name: 'Export' }).click(); - await page.getByText('Which format would you like to export as?').click(); - await page.locator('.app').press('Escape'); + await page.getByRole('button', { name: 'Workspace actions menu button' }).click(); + await page.getByRole('menuitem', { name: 'Export' }).click(); + await page.getByRole('button', { name: 'Export' }).click(); + await page.getByText('Which format would you like to export as?').click(); + await page.locator('.app').press('Escape'); - await page.getByText('Pre-request Scripts').click(); + await page.getByText('Pre-request Scripts').click(); - await page.getByLabel('Request Collection').getByTestId('Long running task').press('Enter'); - await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click(); + await page.getByLabel('Request Collection').getByTestId('Long running task').press('Enter'); + await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click(); - await page.getByRole('button', { name: 'Cancel Request' }).click(); - await page.click('text=Request was cancelled'); -}); - -test('handle hidden browser window getting closed', async ({ app, page }) => { - test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); - - const text = await loadFixture('pre-request-collection.yaml'); - await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); - - await page.getByRole('button', { name: 'Create in project' }).click(); - await page.getByRole('menuitemradio', { name: 'Import' }).click(); - await page.locator('[data-test-id="import-from-clipboard"]').click(); - await page.getByRole('button', { name: 'Scan' }).click(); - await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); - - await page.getByTestId('settings-button').click(); - await page.getByLabel('Request timeout (ms)').fill('1'); - await page.getByRole('button', { name: '' }).click(); - - await page.getByText('Pre-request Scripts').click(); - await page.getByLabel('Request Collection').getByTestId('Long running task').press('Enter'); - await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click(); - - await page.getByText('Timeout: Pre-request script took too long').click(); - await page.getByRole('tab', { name: 'Timeline' }).click(); - await page.getByRole('tab', { name: 'Preview ' }).click(); - const windows = await app.windows(); - const hiddenWindow = windows[1]; - hiddenWindow.close(); - await page.getByRole('button', { name: 'Send' }).click(); - // as the hidden window is restarted, it should not show "Timeout: Hidden browser window is not responding" - await page.getByText('Timeout: Pre-request script took too long').click(); + await page.getByRole('button', { name: 'Cancel Request' }).click(); + await page.click('text=Request was cancelled'); + }); + + test('handle hidden browser window getting closed', async ({ app, page }) => { + test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); + + const text = await loadFixture('pre-request-collection.yaml'); + await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); + + await page.getByRole('button', { name: 'Create in project' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); + await page.getByRole('button', { name: 'Scan' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); + + await page.getByTestId('settings-button').click(); + await page.getByLabel('Request timeout (ms)').fill('1'); + await page.getByRole('button', { name: '' }).click(); + + await page.getByText('Pre-request Scripts').click(); + await page.getByLabel('Request Collection').getByTestId('Long running task').press('Enter'); + await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click(); + + await page.getByText('Timeout: Running script took too long').click(); + await page.getByRole('tab', { name: 'Timeline' }).click(); + await page.getByRole('tab', { name: 'Preview ' }).click(); + const windows = await app.windows(); + const hiddenWindow = windows[1]; + hiddenWindow.close(); + await page.getByRole('button', { name: 'Send' }).click(); + // as the hidden window is restarted, it should not show "Timeout: Hidden browser window is not responding" + await page.getByText('Timeout: Running script took too long').click(); + }); + + test('window should be restarted if it hangs', async ({ app, page }) => { + test.slow(process.platform === 'darwin' || process.platform === 'win32', 'Slow app start on these platforms'); + + // load collection + const text = await loadFixture('pre-request-collection.yaml'); + await app.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text); + + await page.getByRole('button', { name: 'Create in project' }).click(); + await page.getByRole('menuitemradio', { name: 'Import' }).click(); + await page.locator('[data-test-id="import-from-clipboard"]').click(); + await page.getByRole('button', { name: 'Scan' }).click(); + await page.getByRole('dialog').getByRole('button', { name: 'Import' }).click(); + + // update timeout + await page.getByTestId('settings-button').click(); + await page.getByLabel('Request timeout (ms)').fill('100'); + await page.getByRole('button', { name: '' }).click(); + + // send the request with infinite loop script + await page.getByText('Pre-request Scripts').click(); + await page.getByLabel('Request Collection').getByTestId('infinite loop').press('Enter'); + await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click(); + await page.getByText('Timeout: Hidden browser window is not responding').click(); + + // send the another script with normal script + await page.getByLabel('Request Collection').getByTestId('simple log').press('Enter'); + await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click(); + + // it should still work + const statusTag = page.locator('[data-testid="response-status-tag"]:visible'); + await page.waitForSelector('[data-testid="response-status-tag"]:visible'); + await expect(statusTag).toContainText('200 OK'); + }); }); diff --git a/packages/insomnia/src/hidden-window.ts b/packages/insomnia/src/hidden-window.ts index 808d0af78..f5bdba1a1 100644 --- a/packages/insomnia/src/hidden-window.ts +++ b/packages/insomnia/src/hidden-window.ts @@ -15,7 +15,7 @@ window.bridge.onmessage(async (data, callback) => { const timeout = data.context.timeout || 5000; const timeoutPromise = new window.bridge.Promise(resolve => { setTimeout(() => { - resolve({ error: 'Timeout: Pre-request script took too long' }); + resolve({ error: 'Timeout: Running script took too long' }); }, timeout); }); const result = await window.bridge.Promise.race([timeoutPromise, runPreRequestScript(data)]); diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index 4cb27f69d..96b2f5386 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -50,6 +50,17 @@ interface Bounds { export function init() { initLocalStorage(); } +const stopAndWaitForHiddenBrowserWindow = async (runningHiddenBrowserWindow: BrowserWindow) => { + return await new Promise(resolve => { + // overwrite the closed handler + runningHiddenBrowserWindow.on('closed', () => { + console.log('[main] restarting hidden browser window:', runningHiddenBrowserWindow.id); + browserWindows.delete('HiddenBrowserWindow'); + resolve(); + }); + stopHiddenBrowserWindow(); + }); +}; export async function createHiddenBrowserWindow() { const mainWindow = browserWindows.get('Insomnia'); @@ -63,36 +74,28 @@ export async function createHiddenBrowserWindow() { ipcMain.removeHandler('open-channel-to-hidden-browser-window'); // when the main window runs a script // if the hidden window is down, start it - ipcMain.handle('open-channel-to-hidden-browser-window', async event => { - // sync the hidden window status - const runningHiddenWindow = browserWindows.get('HiddenBrowserWindow'); - const isAvailable = !hiddenWindowIsBusy && runningHiddenWindow; - if (isAvailable) { - return; - } - const isOccupied = hiddenWindowIsBusy && runningHiddenWindow; - if (isOccupied) { - // stop and sync the map - await new Promise(resolve => { - invariant(runningHiddenWindow, 'hiddenBrowserWindow is running'); - // overwrite the closed handler - runningHiddenWindow.on('closed', () => { - if (runningHiddenWindow) { - console.log('[main] restarting hidden browser window:', runningHiddenWindow.id); - browserWindows.delete('HiddenBrowserWindow'); - } - resolve(); - }); - stopHiddenBrowserWindow(); - hiddenWindowIsBusy = false; - }); - } - - const windowWasClosedUnexpectedly = hiddenWindowIsBusy && !runningHiddenWindow; + ipcMain.handle('open-channel-to-hidden-browser-window', async (event, isPortAlive: boolean) => { + const runningHiddenBrowserWindow = browserWindows.get('HiddenBrowserWindow'); + const isRunning = !!runningHiddenBrowserWindow; + // if window crashed + const windowWasClosedUnexpectedly = hiddenWindowIsBusy && !isRunning; if (windowWasClosedUnexpectedly) { hiddenWindowIsBusy = false; } + const hiddenWindowIsNotBusy = !hiddenWindowIsBusy; + const isHealthy = hiddenWindowIsNotBusy && isRunning && isPortAlive; + if (isHealthy) { + return; + } + + // if window froze + const isRunningButUnhealthy = isRunning && !isHealthy; + if (isRunningButUnhealthy) { + // stop and wait for window close event and sync the map and busy status + await stopAndWaitForHiddenBrowserWindow(runningHiddenBrowserWindow); + } + console.log('[main] hidden window is down, restarting'); const hiddenBrowserWindow = new BrowserWindow({ show: false, @@ -160,6 +163,7 @@ export async function createHiddenBrowserWindow() { export function stopHiddenBrowserWindow() { browserWindows.get('HiddenBrowserWindow')?.close(); + hiddenWindowIsBusy = false; } export function createWindow(): ElectronBrowserWindow { diff --git a/packages/insomnia/src/preload.ts b/packages/insomnia/src/preload.ts index c2680e25e..375fb195b 100644 --- a/packages/insomnia/src/preload.ts +++ b/packages/insomnia/src/preload.ts @@ -72,7 +72,8 @@ const main: Window['main'] = { }, hiddenBrowserWindow: { runPreRequestScript: options => new Promise(async (resolve, reject) => { - await ipcRenderer.invoke('open-channel-to-hidden-browser-window'); + const isPortAlive = ports.get('hiddenWindowPort') !== undefined; + await ipcRenderer.invoke('open-channel-to-hidden-browser-window', isPortAlive); const port = ports.get('hiddenWindowPort'); invariant(port, 'hiddenWindowPort is undefined');