Chore: replace spectron with playwright (#4305)

* add smoke test fixture

* respect INSOMNIA_DATA_PATH override in CI

* add playwright

* remove spectron

* move CLI tests after app smoke tests in CI

* remove onboarding skip

* random path feedback

* npx feedback

* remove DATA_PATH override

* remove step from import process

* cleanup

* restore readme

* move specs to tests

* feedback on DESIGNER_DATA_PATH

* remove skipLibCheck

* last mention of spectron

* fix windows npm run test:smoke:build

* DATA_PATH override is required

* github CI is slow sometimes
This commit is contained in:
Jack Kavanagh 2021-12-17 13:05:14 +01:00 committed by GitHub
parent b2c94ebbdb
commit 8cea5edc26
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 2111 additions and 2893 deletions

View File

@ -61,6 +61,12 @@ jobs:
- name: Run tests
run: npm test
- name: Build app for smoke tests
run: npm run app-build:smoke
- name: Run app smoke tests
run: npm run test:smoke:build
- name: Set Inso CLI variables
id: inso-variables
shell: bash
@ -133,13 +139,6 @@ jobs:
- name: Run Inso CLI smoke tests
run: npm run test:smoke:cli
- name: Build app for smoke tests
run: npm run app-build:smoke
- name: Run app smoke tests
timeout-minutes: 10 # sometimes jest fails to exit - https://github.com/facebook/jest/issues/6423#issuecomment-620407580
run: npm run test:smoke:build
- name: Upload smoke test screenshots
uses: actions/upload-artifact@v2
if: always()

View File

@ -57,7 +57,7 @@ Insomnia stores data in a few places:
## Automated testing
We use [Jest](https://jestjs.io/) and [react-testing-library](https://testing-library.com/docs/react-testing-library)
to write our unit tests, and [Spectron](https://www.electronjs.org/spectron) for integration tests.
to write our unit tests, and [Playwright](https://github.com/microsoft/playwright) for integration tests.
Unit tests exist alongside the file under test. For example:

View File

@ -37,7 +37,7 @@ log.info(`Running version ${getAppVersion()}`);
if (!isDevelopment()) {
const defaultPath = app.getPath('userData');
const newPath = path.join(defaultPath, '../', appConfig.userDataFolder);
app.setPath('userData', newPath);
app.setPath('userData', process.env.INSOMNIA_DATA_PATH ?? newPath);
}
// So if (window) checks don't throw
@ -145,12 +145,15 @@ function _launchApp() {
commandLineArgs.length && window.send('run-command', commandLineArgs[0]);
});
// Called when second instance launched with args (Windows)
// @TODO: Investigate why this closes electron when using playwright (tested on macOS)
// and find a better solution.
if (!process.env.PLAYWRIGHT) {
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
console.error('[app] Failed to get instance lock');
return;
}
}
app.on('second-instance', () => {
// Someone tried to run a second instance, we should focus our window.

View File

@ -0,0 +1 @@
playwright_skip_browser_download=true

View File

@ -2,17 +2,16 @@
This project contains the smoke testing suite for Insomnia and Inso.
Tests for the Electron app are written using [Spectron](https://github.com/electron-userland/spectron#application-api) (and [spectron-keys](https://github.com/jsantell/spectron-keys) for key inputs), while tests for the CLI use [execa](https://github.com/sindresorhus/execa).
Tests for the Electron app are written using [Playwright](https://github.com/microsoft/playwright) while tests for the CLI use [execa](https://github.com/sindresorhus/execa).
## Structure
| Folder | Purpose |
| - | - |
| ----------- | --------------------------------- |
| `/cli` | tests for inso |
| `/core` | tests for Insomnia |
| `/tests` | tests for Insomnia |
| `/server` | Express server used by the tests |
| `/fixtures` | data used by tests and the server |
| `/modules` | logical grouping of functionality (eg. `modals` , `tabs` , `settings` , `home` ) |
## How to run
@ -31,6 +30,8 @@ You can then run the smoke tests, again from the root:
```shell
npm run test:smoke:cli # Run CLI tests
npm run test:smoke:build # Run Insomnia tests
PWDEBUG=1 npm run test:smoke:build # Write Insomnia tests with the playwrite recorder
DEBUG=pw:browser,pw:api npm run test:smoke:build # Run Insomnia tests, with verbose output
```
Sometimes, you might need to run tests against a _packaged_ application. A packaged application is the final artifact which bundles all of the various resources together, and is created for distribution in the form of a `.dmg` or `.exe`, etc. Packaging takes longer to do and is only required for edge cases (such as a <!-- TODO(TSCONVERSION) update this link -->[plugin installation](https://github.com/Kong/insomnia/blob/357b8f05f89fd5c07a75d8418670abe37b2882dc/packages/insomnia-smoke-test/designer/app.test.js#L36)), so we typically run tests against a build. To run packaged tests, from the root:
@ -46,7 +47,7 @@ Each of the above commands will automatically run the Express server, so you do
When writing tests, it is recommended to use the scripts in this project directly (instead of from the root, as per the section above). After building and/or packaging your application under test, it will be available under `packages/insomnia-app/{build|dist}` and you can begin writing your test.
In order to run tests for development, open two terminal tabs in `packages/insomnia-smoke-test`:
In order to run CLI tests for development, open two terminal tabs in `packages/insomnia-smoke-test`:
```shell
# In the first tab, serve the Express API
@ -54,15 +55,10 @@ npm run serve
# In the second tab, run your tests
npm run cli # Run CLI tests
npm run spectron:build # Insomnia build tests
npm run spectron:package # Insomnia package tests
```
This will allow you to write and monitor the server separately from each test, speeding up the development cycle.
You may also need to run a test multiple times. You can focus a particular test or test suite using [Jest globals](https://jestjs.io/docs/en/api#testonlyname-fn-timeout) such as `it.only` and `describe.skip`, or their aliases `fit` and `xdescribe`, etc.
## General guidelines
### Data
@ -73,139 +69,6 @@ Individual tests will automatically run against a clean Insomnia data directory
A test should not depend on any external services unless absolutely necessary. If a particular endpoint is required (eg. for authentication or a specific content type), implement a new endpoint in `/server`.
### Element selection
Spectron is built heavily on top of WebdriverIO, and WebdriverIO's `browser` object is available under `app.client` ([docs](https://github.com/electron-userland/spectron#client)). This is the primary API you will need for user interactions, see examples with existing tests.
Through WebdriverIO you can use a host of CSS or React selectors. There is no clear guideline about which selector to use, but whichever approach is used it must favour stability and be understandable.
There are trade-offs with each selector approach but it's important to know how generic or specific a particular component or CSS class is, in order to ensure that the correct element is always selected as the application evolves.
#### Select by component and props
Sometimes selecting by a React component and props, directly from `app.client` is the cleanest approach, as the following two examples show:
```ts
const waitUntilRequestIsActive = async (app: Application, name: string) => {
const request = await app.client.react$('UnconnectedSidebarRequestRow', {
props: { isActive: true, request: { name } },
});
await request.waitForDisplayed();
};
export const clickFolderByName = async (app, name) => {
const folder = await app.client.react$('UnconnectedSidebarRequestGroupRow', {
props: { requestGroup: { name } },
});
await folder.waitForClickable();
await folder.click();
};
```
You can find a list of component names in `modules/component-names.ts`.
#### Scoping
It is important to scope an element to an appropriate ancestor. In a way the selector becomes self-documenting, but also ensures stability as the UI evolves.
In the following example, it is possible for multiple buttons which match the `button#enabled` selector to exist on the page. By chaining a React and CSS selector, we can ensure the test runner will always click the expected button within the `BasicAuth` component.
```ts
export const toggleBasicAuthEnabled = async (app: Application) => {
await app.client
.react$('BasicAuth')
.then(e => e.$('button#enabled'))
.then(e => e.click());
};
```
A similar approach can be achieved through a CSS selector. In the following example, after sending a successful request, we want to detect an element containing the CSS classes `tag bg-success` and ensure it contains the text `200 OK`.
These classes are fairly generic and could exist multiple times on the page, but the HTTP response code will always be in the response pane (`response-pane`) header (`pane__header`). As such, the selector is scoped to always select the expected element, wait for it to show, and ensure it has the expected text.
```ts
export const expect200 = async (app: Application) => {
const tag = await app.client.$('.response-pane .pane__header .tag.bg-success');
await tag.waitForDisplayed();
await expectText(tag, '200 OK');
};
```
### Interactions
As is common with all smoke testing frameworks, before interacting with an element (click, hover, etc) it is generally good to check whether you _can_ interact with it. For instance, clicking a button will fail if the button is not yet clickable.
Sometimes you will need to add explicit pauses to allow for UI to refresh or database writes to occur (`await app.client.pause(500)`). Try to keep these to a minimum, though, exploring all other avenues first, such as WebdriverIO's `waitFor*` functions. Avoiding explicit waits ensures each test runs in the short amount of time.
When typing in the url bar for HTTP requests, we first wait for it to exist on the page before clicking on it and typing, because request activation can take some time.
```ts
export const typeInUrlBar = async (app: Application, url: string) => {
const urlEditor = await app.client.react$('RequestUrlBar');
await urlEditor.waitForExist();
await urlEditor.click();
await urlEditor.keys(url);
};
```
In addition, sometimes we want to wait for an element to hide instead of show. To achieve this, we can use the `reverse` option available through WebdriverIO, as shown in the following example.
```ts
// Wait for spinner to show
const spinner = await app.client.react$('ResponseTimer');
await spinner.waitForDisplayed();
// Wait for spinner to hide
await spinner.waitForDisplayed({ reverse: true });
```
### Readability
It is important for a smoke test to be _readable_ so the flow can be understood, and the (often complicated) implementation details hidden, like in the example below.
```ts
import * as debug from '../modules/debug';
it('sends request with basic authentication', async () => {
const url = 'http://127.0.0.1:4010/auth/basic';
const { latin1, utf8 } = basicAuthCreds;
await debug.workspaceDropdownExists(app);
await debug.createNewRequest(app, 'basic-auth');
await debug.typeInUrlBar(app, url);
// Send request with no auth present
await debug.clickSendRequest(app);
await debug.expect401(app);
// Click auth tab
await debug.clickRequestAuthTab(app);
await debug.expectNoAuthSelected(app);
// Select basic auth
await debug.clickRequestAuthDropdown(app);
await debug.clickBasicAuth(app);
// Enter username and password with regular characters
await debug.typeBasicAuthUsernameAndPassword(app, utf8.raw.user, utf8.raw.pass);
// Send request with auth present
await debug.clickSendRequest(app);
await debug.expect200(app);
const responseViewer = await debug.getResponseViewer(app);
await debug.expectText(responseViewer, '1\nbasic auth received');
});
```
In most cases, it will be beneficial to create helper functions under `/modules`, regardless of how reusable they are. Some modules (such as `dropdown`, `tabs` and `settings`) are reusable, while some are specific to certain pages (eg `debug`, `home`, `onboarding`). These can be broken down into more granular modules as the test suite grows.
### Extend tests
Unlike unit tests, the application startup time for a smoke test can sometimes be longer than the test itself. As such, in cases where it is appropriate, **extend** a smoke test with additional steps instead of creating a **new** test.
## Working with fixtures
### How to update the inso-nedb fixture
@ -235,26 +98,8 @@ Set the `--src packages/insomnia-smoke-test/fixtures/inso-nedb` flag
```bash
# if installed globally
inso --src ...
# using the package bin
./packages/insomnia-inso/bin/inso --src ...
# using a binary
./packages/insomnia-inso/binaries/insomnia-inso --src ...
```
## Contributing a smoke test?
Smoke tests can potentially be flaky, and one attempt to avoid flaky tests in the default branch is to run the final implementation of a test at least 20 times locally to prove its stability. If a test is unable to achieve this, it is very unlikely to be accepted into the test suite.
You can repeat a test quickly by wrapping it with the following block:
```ts
describe.only.each(new Array(20).fill(1))('iteration %#', _ => {
it('your test name', () => {
//...
});
});
```
When raising a PR, paste a screenshot of the test results showing at least 20 successful iterations.

View File

@ -1,213 +0,0 @@
import { Application } from 'spectron';
import { basicAuthCreds } from '../fixtures/constants';
import { launchApp, stop } from '../modules/application';
import * as client from '../modules/client';
import * as debug from '../modules/debug';
import * as dropdown from '../modules/dropdown';
import * as home from '../modules/home';
import * as modal from '../modules/modal';
import * as onboarding from '../modules/onboarding';
import { waitUntilTextDisappears } from '../modules/text';
describe('Application launch', function() {
jest.setTimeout(50000);
let app: Application;
beforeEach(async () => {
app = await launchApp();
});
afterEach(async () => {
await stop(app);
});
it('shows an initial window', async () => {
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
});
it('sends JSON request', async () => {
const url = 'http://127.0.0.1:4010/pets/{% now \'millis\', \'\' %}';
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
await home.createNewCollection(app);
await debug.pageDisplayed(app);
await debug.createNewRequest(app, 'json');
await debug.typeInUrlBar(app, url);
await debug.clickSendRequest(app);
await debug.expect200(app);
});
it('sends dummy.csv request and shows rich response', async () => {
const url = 'http://127.0.0.1:4010/file/dummy.csv';
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
await home.createNewCollection(app);
await debug.pageDisplayed(app);
await debug.createNewRequest(app, 'csv');
await debug.typeInUrlBar(app, url);
await debug.clickSendRequest(app);
await debug.expect200(app);
const csvViewer = await debug.getCsvViewer(app);
await expect(csvViewer.getText()).resolves.toBe('a b c\n1 2 3');
});
it('sends dummy.xml request and shows raw response', async () => {
const url = 'http://127.0.0.1:4010/file/dummy.xml';
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
await home.createNewCollection(app);
await debug.pageDisplayed(app);
await debug.createNewRequest(app, 'xml');
await debug.typeInUrlBar(app, url);
await debug.clickSendRequest(app);
await debug.expect200(app);
const responseViewer = await debug.getResponseViewer(app);
const partialExpectedResponse = '<LoginResult>xxx-777-xxx-123</LoginResult>';
await debug.expectContainsText(responseViewer, partialExpectedResponse);
await debug.typeInResponseFilter(app, "//*[local-name(.)='LoginResult']/text()");
await waitUntilTextDisappears(app, responseViewer, partialExpectedResponse);
await debug.expectContainsText(
responseViewer,
'<result>xxx-777-xxx-123</result>',
);
});
it('sends dummy.pdf request and shows rich response', async () => {
const url = 'http://127.0.0.1:4010/file/dummy.pdf';
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
await home.createNewCollection(app);
await debug.pageDisplayed(app);
await debug.createNewRequest(app, 'pdf');
await debug.typeInUrlBar(app, url);
await debug.clickSendRequest(app);
await debug.expect200(app);
const pdfCanvas = await debug.getPdfCanvas(app);
// Investigate how we can extract text from the canvas, or compare images
await expect(pdfCanvas.isExisting()).resolves.toBe(true);
});
// NOTE: skipped because plugins are pulled from npm in CI rather than read from this repo
// TODO: unskip this test after ticket INS-502 corrects the above
it.skip('shows deploy to dev portal for design documents', async () => {
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
const docName = await home.createNewDocument(app);
await debug.goToDashboard(app);
// Open card dropdown for the document
const card = await home.findCardWithTitle(app, docName);
await home.openWorkspaceCardDropdown(card);
// Click the "Deploy to Dev Portal" button, installed from that plugin
await dropdown.clickOpenDropdownItemByText(app, 'Deploy to Dev Portal');
// Ensure a modal opens, then close it - the rest is plugin behavior
await modal.waitUntilOpened(app, { title: 'Deploy to Dev Portal' });
await modal.close(app);
});
// This test will ensure that for an endpoint which expects basic auth:
// 1. sending no basic auth will fail
// 2. sending basic auth will succeed
// 3. sending basic auth with special characters encoded with IS0-8859-1 will succeed
// 4. sending while basic auth is disabled within insomnia will fail
// TODO(TSCONVERSION) - this test fails fairly readily after TS conversion, needs investigation
it.skip('sends request with basic authentication', async () => {
const url = 'http://127.0.0.1:4010/auth/basic';
const { latin1, utf8 } = basicAuthCreds;
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
await home.createNewCollection(app);
await debug.pageDisplayed(app);
await debug.createNewRequest(app, 'basic-auth');
await debug.typeInUrlBar(app, url);
// Send request with no auth present
await debug.clickSendRequest(app);
await debug.expect401(app);
// Click auth tab
await debug.clickRequestAuthTab(app);
await debug.expectNoAuthSelected(app);
// Select basic auth
await debug.clickRequestAuthDropdown(app);
await debug.clickBasicAuth(app);
// Enter username and password with regular characters
await debug.typeBasicAuthUsernameAndPassword(app, utf8.raw.user, utf8.raw.pass);
// Send request with auth present
await debug.clickSendRequest(app);
await debug.expect200(app);
const responseViewer = await debug.getResponseViewer(app);
await debug.expectText(responseViewer, '1\nbasic auth received');
// Check auth header in timeline
await debug.clickTimelineTab(app);
await debug.expectContainsText(
await debug.getTimelineViewer(app),
`> Authorization: Basic ${utf8.combined}`,
);
// Clear inputs and type username/password with special characters
await debug.typeBasicAuthUsernameAndPassword(app, latin1.raw.user, latin1.raw.pass, true);
// Toggle basic auth and encoding enabled
await debug.toggleBasicAuthEncoding(app);
// Send request
await debug.clickSendRequest(app);
await debug.expect200(app);
await debug.expectContainsText(
await debug.getTimelineViewer(app),
`> Authorization: Basic ${latin1.combined}`,
);
// Toggle basic auth to disabled
await debug.toggleBasicAuthEnabled(app);
// Send request
await debug.clickSendRequest(app);
await debug.expect401(app);
await debug.expectNotContainsText(await debug.getTimelineViewer(app), '> Authorization: Basic');
});
});

View File

@ -1,273 +0,0 @@
import { Application } from 'spectron';
import { launchApp, stop, writeTextToClipboard } from '../modules/application';
import * as client from '../modules/client';
import * as debug from '../modules/debug';
import * as design from '../modules/design';
import { loadFixture } from '../modules/fixtures';
import * as home from '../modules/home';
import * as modal from '../modules/modal';
import * as onboarding from '../modules/onboarding';
import * as settings from '../modules/settings';
describe.only('Import', function() {
jest.setTimeout(50000);
let app: Application;
beforeEach(async () => {
app = await launchApp();
});
afterEach(async () => {
await stop(app);
});
describe('from the Dashboard', () => {
it('should create a new workspace if there are no available workspaces', async () => {
// Launch the app
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
await home.expectTotalDocuments(app, 0);
// Import the fixture from the clipboard
const swagger2Text = await loadFixture('swagger2.yaml');
writeTextToClipboard(app, swagger2Text);
await home.importFromClipboard(app);
// Import as a design document
await modal.waitUntilOpened(app, { title: 'Import As' });
await modal.clickModalFooterByText(app, 'Design Document');
await home.expectTotalDocuments(app, 1);
// Open the new document and send a request
await home.openDocumentWithTitle(app, 'E2E testing specification - swagger 2 1.0.0');
await design.goToActivity(app, 'debug');
await debug.clickFolderByName(app, 'custom-tag');
await debug.clickRequestByName(app, 'get pet by id');
await debug.clickSendRequest(app);
await debug.expect200(app);
});
it('should prompt the user to import to a new workspace', async () => {
// Launch the app
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
// Create a document
await home.createNewCollection(app);
await debug.goToDashboard(app);
await home.expectTotalDocuments(app, 1);
// Import the fixture from the clipboard
const swagger2Text = await loadFixture('swagger2.yaml');
writeTextToClipboard(app, swagger2Text);
await home.importFromClipboard(app);
// Chose to import to a new workspace as a design document
await modal.waitUntilOpened(app, { title: 'Import' });
await modal.clickModalFooterByText(app, 'New');
await modal.waitUntilOpened(app, { title: 'Import As' });
await modal.clickModalFooterByText(app, 'Design Document');
await home.expectTotalDocuments(app, 2);
// Open the new document and send a request
await home.openDocumentWithTitle(app, 'E2E testing specification - swagger 2 1.0.0');
await design.goToActivity(app, 'debug');
await debug.clickFolderByName(app, 'custom-tag');
await debug.clickRequestByName(app, 'get pet by id');
await debug.clickSendRequest(app);
await debug.expect200(app);
});
it('should prompt the user to import to an existing workspace', async () => {
// Launch the app
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
// Create a document
const newCollectionName = await home.createNewCollection(app, 'New');
await debug.goToDashboard(app);
await home.expectTotalDocuments(app, 1);
// Import the fixture from the clipboard
const swagger2Text = await loadFixture('swagger2.yaml');
writeTextToClipboard(app, swagger2Text);
await home.importFromClipboard(app);
// Chose to import to the existing document
await modal.waitUntilOpened(app, { title: 'Import' });
await modal.clickModalFooterByText(app, 'Existing');
await modal.selectModalOption(app, newCollectionName);
await modal.clickModalFooterByText(app, 'Done');
await home.expectTotalDocuments(app, 1);
// Since the workspace we import has a name it overrides the existing one
// Open the new document and send a request
await home.openDocumentWithTitle(app, 'E2E testing specification - swagger 2 1.0.0');
await debug.clickFolderByName(app, 'custom-tag');
await debug.clickRequestByName(app, 'get pet by id');
await debug.clickSendRequest(app);
await debug.expect200(app);
});
it('should update the existing workspace (e.g. Insomnia Exports)', async () => {
// Launch the app
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
await home.expectTotalDocuments(app, 0);
// Import the fixture from the clipboard
const insomnia4Text = await loadFixture('insomnia4.yaml');
writeTextToClipboard(app, insomnia4Text);
await home.importFromClipboard(app);
// Chose to import as a collection
await modal.waitUntilOpened(app, { title: 'Import As' });
await modal.clickModalFooterByText(app, 'Request Collection');
await home.expectTotalDocuments(app, 1);
// Import the spec a second time
await home.importFromClipboard(app);
// It should update the existing document
await home.expectTotalDocuments(app, 1);
// Open the new document and send a request
await home.openDocumentWithTitle(app, 'Insomnia');
await debug.clickFolderByName(app, 'Actions');
await debug.clickRequestByName(app, 'Get Sleep');
await debug.clickSendRequest(app);
await debug.expect200(app);
});
});
describe('from Preferences', () => {
it('should directly import to the active workspace', async () => {
// Launch the app
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
await home.expectTotalDocuments(app, 0);
// Create a document and open it
await home.createNewCollection(app);
// Import the fixture from the preferences panel
const swagger2Text = await loadFixture('swagger2.yaml');
writeTextToClipboard(app, swagger2Text);
await settings.openFromSettingsButton(app);
await settings.goToDataTab(app);
await settings.importFromClipboard(app);
// Send a request
await debug.clickFolderByName(app, 'custom-tag');
await debug.clickRequestByName(app, 'get pet by id');
await debug.clickSendRequest(app);
await debug.expect200(app);
// Navigate to the dashboard and check no other workspace was created
await debug.goToDashboard(app);
await home.expectTotalDocuments(app, 1);
});
it('should import an existing workspace to the project instead of the current workspace (e.g. Insomnia Exports)', async () => {
// Launch the app
await client.correctlyLaunched(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
await home.expectTotalDocuments(app, 0);
// Import the fixture from the clipboard
const insomnia4Text = await loadFixture('insomnia4.yaml');
writeTextToClipboard(app, insomnia4Text);
await home.importFromClipboard(app);
await modal.waitUntilOpened(app, { title: 'Import As' });
await modal.clickModalFooterByText(app, 'Design Document');
await home.expectTotalDocuments(app, 1);
// Create a document and open it
await home.createNewCollection(app);
// Import the updated fixture from the preferences panel
const insomnia4UpdatedText = await loadFixture('insomnia4-update.yaml');
writeTextToClipboard(app, insomnia4UpdatedText);
await settings.openFromSettingsButton(app);
await settings.goToDataTab(app);
await settings.importFromClipboard(app);
// NOTE: The document was imported directly to the
// previously imported workspace and the user didn't see a dialog
// Open the previous document and check that it's updated
await debug.goToDashboard(app);
await home.expectTotalDocuments(app, 2);
await home.openDocumentWithTitle(app, 'Insomnia');
await design.goToActivity(app, 'debug');
await debug.clickFolderByName(app, 'Actions');
await debug.clickRequestByName(app, 'Get Sleep 2');
await debug.clickSendRequest(app);
await debug.expect200(app);
});
});
});

View File

@ -1,66 +0,0 @@
import path from 'path';
import { Application } from 'spectron';
import { launchApp, stop } from '../modules/application';
import * as client from '../modules/client';
import * as home from '../modules/home';
import * as migration from '../modules/migration';
import * as onboarding from '../modules/onboarding';
describe('Migration', function() {
jest.setTimeout(50000);
let app: Application;
beforeEach(async () => {
app = await launchApp(path.join(__dirname, '..', 'fixtures', 'basic-designer'));
});
afterEach(async () => {
await stop(app);
});
it('can skip migration and proceed onboarding', async () => {
await client.correctlyLaunched(app);
await migration.migrationMessageShown(app);
await migration.clickSkip(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
await home.expectTotalDocuments(app, 0);
await app.restart();
await client.focusAfterRestart(app);
await home.documentListingShown(app);
});
it('can migrate and proceed onboarding', async () => {
await client.correctlyLaunched(app);
await migration.migrationMessageShown(app);
await migration.ensureStartNotClickable(app);
await migration.toggleOption(app, 'Copy Workspaces');
await migration.toggleOption(app, 'Copy Plugins');
await migration.toggleOption(app, 'Copy Designer Application Settings');
await migration.clickStart(app);
await migration.successMessageShown(app);
await migration.clickRestart(app);
await client.focusAfterRestart(app);
await onboarding.skipOnboardingFlow(app);
await home.documentListingShown(app);
await home.expectTotalDocuments(app, 1);
await home.expectDocumentWithTitle(app, 'BASIC-DESIGNER-FIXTURE'); // imported from fixture
await app.restart();
await client.focusAfterRestart(app);
await home.documentListingShown(app);
});
});

View File

@ -1,47 +0,0 @@
import { Application } from 'spectron';
import { isPackage, launchApp, stop } from '../modules/application';
import * as client from '../modules/client';
import * as dropdown from '../modules/dropdown';
import * as home from '../modules/home';
import * as modal from '../modules/modal';
import * as settings from '../modules/settings';
const itIf = condition => (condition ? it : it.skip);
// @ts-expect-error TSCONVERSION
it.if = itIf;
xdescribe('Application launch', function() {
jest.setTimeout(50000);
let app: Application;
beforeEach(async () => {
app = await launchApp();
});
afterEach(async () => {
await stop(app);
});
// @ts-expect-error TSCONVERSION
xit.if(isPackage())('can install and consume a plugin', async () => {
await client.correctlyLaunched(app);
await home.documentListingShown(app);
// Install plugin
await settings.openWithKeyboardShortcut(app);
await settings.goToPluginsTab(app);
await settings.installPlugin(app, 'insomnia-plugin-kong-portal');
await settings.closeModal(app);
// Open card dropdown for any card
const dd = await home.openWorkspaceCardDropdown(app);
// Click the "Deploy to Dev Portal" button, installed from that plugin
await dropdown.clickDropdownItemByText(dd, 'Deploy to Dev Portal');
// Ensure a modal opens, then close it - the rest is plugin behavior
await modal.waitUntilOpened(app, { title: 'Deploy to Dev Portal' });
await modal.close(app);
});
});

View File

@ -0,0 +1,190 @@
_type: export
__export_format: 4
__export_date: 2021-12-03T07:42:43.250Z
__export_source: insomnia.desktop.app:v2021.6.0
resources:
- _id: req_178f98ccc6b240f5a1f67c492b6bf9a6
parentId: wrk_5b5ab67830944ffcbec47528366ef403
modified: 1636142586648
created: 1636141100570
url: http://127.0.0.1:4010/auth/basic
name: sends request with basic authentication
description: ""
method: GET
body: {}
parameters: []
headers:
- id: pair_cbcd8bc34d494a3c83b29989d06cf005
name: Authorization
value: Basic dXNlcjpwYXNz
description: ""
disabled: true
authentication:
type: basic
useISO88591: false
disabled: false
username: user
password: pass
metaSortKey: -1636141100570
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: wrk_5b5ab67830944ffcbec47528366ef403
parentId: null
modified: 1636140994423
created: 1636140994423
name: Smoke tests
description: ""
scope: collection
_type: workspace
- _id: req_e6fab3568ed54f9d83c92e2c8006e140
parentId: wrk_5b5ab67830944ffcbec47528366ef403
modified: 1636141084436
created: 1636141078601
url: http://127.0.0.1:4010/file/dummy.pdf
name: sends dummy.pdf request and shows rich response
description: ""
method: GET
body: {}
parameters: []
headers: []
authentication: {}
metaSortKey: -1636141078601
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: req_06a660bffe724e3fac14d7d09b4a5c9b
parentId: wrk_5b5ab67830944ffcbec47528366ef403
modified: 1636141067089
created: 1636141061433
url: http://127.0.0.1:4010/file/dummy.xml
name: sends dummy.xml request and shows raw response
description: ""
method: GET
body: {}
parameters: []
headers: []
authentication: {}
metaSortKey: -1636141061433
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: req_ee610ff89152476ea25950f39ca59819
parentId: wrk_5b5ab67830944ffcbec47528366ef403
modified: 1636141047337
created: 1636141038448
url: http://127.0.0.1:4010/file/dummy.csv
name: sends dummy.csv request and shows rich response
description: ""
method: GET
body: {}
parameters: []
headers: []
authentication: {}
metaSortKey: -1636141038448
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: req_89dade2ee9ee42fbb22d588783a9df3b
parentId: wrk_5b5ab67830944ffcbec47528366ef403
modified: 1636707449231
created: 1636141014552
url: http://127.0.0.1:4010/pets/1
name: send JSON request
description: ""
method: GET
body: {}
parameters: []
headers:
- id: pair_4c3fe3092f1245eab6d960c633d8be9c
name: test
value: test
description: ""
authentication: {}
metaSortKey: -1636141014552
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: env_afc214be117853a024cce22f194c500e68dac13f
parentId: wrk_5b5ab67830944ffcbec47528366ef403
modified: 1636140994432
created: 1636140994432
name: Base Environment
data: {}
dataPropertyOrder: null
color: null
isPrivate: false
metaSortKey: 1636140994432
_type: environment
- _id: jar_afc214be117853a024cce22f194c500e68dac13f
parentId: wrk_5b5ab67830944ffcbec47528366ef403
modified: 1637279629638
created: 1636140994434
name: Default Jar
cookies:
- key: foo
value: bar
expires: null
domain: domain.com
path: /
creation: 2021-11-18T23:53:05.310Z
id: "429589439757017"
- key: has_recent_activity
value: "1"
expires: 2021-11-19T00:53:49.000Z
domain: github.com
path: /
secure: true
httpOnly: true
extensions:
- SameSite=Lax
hostOnly: true
creation: 2021-11-18T23:53:05.311Z
lastAccessed: 2021-11-18T23:53:49.637Z
id: "09630096071226024"
- key: _gh_sess
value: hno6H6Q2M8RK8eDeSpwNIhlNCDPsh%2BTLYCqW37gBtKvE2iHWyuCzusnHmN2M2zqL5fm4vNek98BlbXrBb2xPI%2Fsx9Q%2FPaIPJBeQ%2BJLlAetGuXnjucDjb7jwVFD07jFxPJ8n8%2F3i6yk2II%2BPyPQOfs5y45g4i5JLxhpu5BE6JGj02Jf8PLgXnvNpILm3QiLyT4oIuxqZFEbko%2BdmdIoG8qSs9hUiLBsYp1KgM8v7Jq7EWbb%2BkmVQDFnDkmvFIV6K5Usu6owlZQwM7XTo5KYef%2FcF63ic1o1%2F5JdDHHPoTsW4h68MFGEcPBqxHeGAa7Cw7LV%2FrnsvrNtsAtfMih%2FYI%2FeKn5jk3ChUN7cV43retCmIApNMd%2FFGAeuf4pmmvqbFLHtPk6zd8dXRSdwPGHp608GpTqWhRrh%2FNMnydgZj86h9668yLRNWeii%2B1JToNnpl8gU6iYNUEJuBPlZJ%2BTT5H4ZtNskY5Ccw8ysPLKH1qUtwXc%2ByJPIvYI1fGPNpX8BcLHhM0m5ru0jdEBvOsXd%2FbPdt%2B0%2BKbQEqUyig3hygN3C7bfAkE3%2B1IOhFYdWcMR83msFf1jvuE%2FoWw%2FGOPHYhiO48ePz7V9ZwqOuBG7ikUyfS07XjeTgqPrO5P5ON7YowCK%2F7x7rt%2Beu84Pi8Uc%2FA4aGhQU%2B8Ixn%2BrvnqYMDLlsYwP%2BTcoHLPEKe4IFDWjoTJehae2%2FQ4WAE88L%2FTgXAeqnO4%2BHaiFtoZYfLDMS4Xm0cB7MRC7C%2BaCvr90j56emc13r1%2Fgeg%3D%3D--ZIqRrsGB9DwAmnTG--xwQhqF9WAvbb5FNbLruEzQ%3D%3D
domain: github.com
path: /
secure: true
httpOnly: true
extensions:
- SameSite=Lax
hostOnly: true
creation: 2021-11-18T23:53:05.312Z
lastAccessed: 2021-11-18T23:53:49.637Z
id: "5403425689099031"
_type: cookie_jar
- _id: spc_255904e6a8774d24b29c9c3718feb07f
parentId: wrk_5b5ab67830944ffcbec47528366ef403
modified: 1636140994428
created: 1636140994428
fileName: Smoke tests
contents: ""
contentType: yaml
_type: api_spec

View File

@ -1,117 +0,0 @@
import fs from 'fs';
import mkdirp from 'mkdirp';
import os from 'os';
import path from 'path';
import { Application } from 'spectron';
// @ts-expect-error TSCONVERSION
import electronPath from '../../insomnia-app/node_modules/electron';
const getAppPlatform = () => process.platform;
const isMac = () => getAppPlatform() === 'darwin';
const isLinux = () => getAppPlatform() === 'linux';
const isWindows = () => getAppPlatform() === 'win32';
export const isBuild = () => process.env.BUNDLE === 'build';
export const isPackage = () => process.env.BUNDLE === 'package';
const spectronConfig = (
designerDataPath = path.join(__dirname, '..', 'fixtures', 'doesnt-exist'),
) => {
let packagePathSuffix = '';
if (isWindows()) {
packagePathSuffix = path.join('win-unpacked', 'Insomnia.exe');
} else if (isMac()) {
packagePathSuffix = path.join('mac', 'Insomnia.app', 'Contents', 'MacOS', 'Insomnia');
} else if (isLinux()) {
packagePathSuffix = ''; // TODO: find out what this is
}
const buildPath = path.join(__dirname, '../../insomnia-app/build');
const packagePath = path.join(__dirname, '../../insomnia-app/dist', packagePathSuffix);
const dataPath = path.join(os.tmpdir(), 'insomnia-smoke-test', `${Date.now()}`);
const env = { INSOMNIA_DATA_PATH: dataPath };
if (designerDataPath) {
// @ts-expect-error TSCONVERSION
env.DESIGNER_DATA_PATH = designerDataPath;
}
return { buildPath, packagePath, env };
};
export const launchApp = async (designerDataPath?: string) => {
const config = spectronConfig(designerDataPath);
return await launch(config);
};
const getLaunchPath = config =>
isPackage()
? { path: config.packagePath }
: {
path: electronPath,
args: [config.buildPath],
};
const launch = async config => {
if (!config) {
throw new Error('Spectron config could not be loaded.');
}
const app = new Application({
...getLaunchPath(config),
// Don't remove chromeDriverArgs
// https://github.com/electron-userland/spectron/issues/353#issuecomment-522846725
chromeDriverArgs: ['remote-debugging-port=9222'],
env: config.env,
startTimeout: 10000,
waitTimeout: 10000,
});
await app.start().then(async () => {
// Windows spawns two terminal windows when running spectron, and the only workaround
// is to focus the window on start.
// https://github.com/electron-userland/spectron/issues/60
await app.browserWindow.focus();
await app.browserWindow.setAlwaysOnTop(true);
// Set the implicit wait timeout to 0 (webdriver default)
// https://webdriver.io/docs/timeouts.html#session-implicit-wait-timeout
// Spectron overrides it to an unreasonable value, as per the issue
// https://github.com/electron-userland/spectron/issues/763
await app.client.setTimeout({ implicit: 0 });
// Set bounds to default size
await app.browserWindow.setSize(1280, 700);
});
return app;
};
export const stop = async app => {
await takeScreenshotOnFailure(app);
if (app?.isRunning()) {
await app.stop();
}
};
const takeScreenshotOnFailure = async app => {
// @ts-expect-error TSCONVERSION
if (jasmine.currentTest.failedExpectations.length) {
// @ts-expect-error TSCONVERSION
await takeScreenshot(app, jasmine.currentTest.fullName.replace(/ /g, '_'));
}
};
export const takeScreenshot = async (app, name) => {
mkdirp.sync('screenshots');
const buffer = await app.browserWindow.capturePage();
await fs.promises.writeFile(path.join('screenshots', `${name}.png`), buffer);
};
export const writeTextToClipboard = (app: Application, text: string) => {
app.electron.clipboard.writeText(text);
};

View File

@ -1,17 +0,0 @@
export const correctlyLaunched = async app => {
await expect(app.browserWindow.isDevToolsOpened()).resolves.toBe(false);
await expect(app.client.getWindowCount()).resolves.toBe(1);
await expect(app.browserWindow.isMinimized()).resolves.toBe(false);
await expect(app.browserWindow.isFocused()).resolves.toBe(true);
};
export const focusAfterRestart = async app => {
await app.client.pause(4000);
const count = await app.client.getWindowCount();
if (count === 0) {
console.log('No windows found');
}
await app.client.windowByIndex(0);
};

View File

@ -1,5 +0,0 @@
export const COMPONENT_NAMES = {
sidebarRequestRow: 'UnconnectedSidebarRequestRow',
sidebarRequestGroupRow: 'UnconnectedSidebarRequestGroupRow',
requestUrlBar: 'RequestUrlBar',
};

View File

@ -1,258 +0,0 @@
import faker from 'faker';
import { Application } from 'spectron';
import spectronKeys from 'spectron-keys';
import { COMPONENT_NAMES } from './component-names';
export const workspaceDropdownExists = async (app: Application, workspaceName = 'Insomnia') => {
await app.client.waitUntilTextExists('.workspace-dropdown', workspaceName);
};
export const pageDisplayed = async (app: Application) => {
await app.client.react$('WrapperDebug').then(e => e.waitForDisplayed());
};
export const clickWorkspaceDropdown = async (app: Application) => {
const dropdown = await app.client.react$('WorkspaceDropdown');
await dropdown.click();
return dropdown;
};
export const goToDashboard = async (app: Application) => {
await app.client.$('.header_left [data-testid="project"]').then(e => e.click());
};
export const createNewRequest = async (app: Application, name: string) => {
await app.client.$('.sidebar .dropdown .fa-plus-circle').then(e => e.click());
await app.client
.$('[aria-hidden=false]')
.then(e => e.$('button*=New Request'))
.then(e => e.click());
// Wait for modal to open
await app.client.waitUntilTextExists('.modal__header', 'New Request');
// Set name and create request
const input = await app.client.$('.modal input');
const requestName = `${name}-${faker.lorem.slug()}`;
await input.waitUntil(() => input.isFocused());
await input.keys(requestName);
await app.client
.$('.modal .modal__footer')
.then(e => e.$('button=Create'))
.then(e => e.click());
await waitUntilRequestIsActive(app, requestName);
};
const waitUntilRequestIsActive = async (app: Application, name: string) => {
const request = await app.client.react$(COMPONENT_NAMES.sidebarRequestRow, {
props: { isActive: true, request: { name } },
});
await request.waitForDisplayed();
};
export const clickFolderByName = async (app: Application, name: string) => {
const folder = await app.client.react$(COMPONENT_NAMES.sidebarRequestGroupRow, {
props: { requestGroup: { name } },
});
await folder.waitForClickable();
await folder.click();
};
export const clickRequestByName = async (app: Application, name: string) => {
const folder = await app.client.react$(COMPONENT_NAMES.sidebarRequestRow, {
props: { request: { name } },
});
await folder.waitForClickable();
await folder.click();
};
export const typeInUrlBar = async (app: Application, url) => {
const urlEditor = await app.client.react$(COMPONENT_NAMES.requestUrlBar);
await urlEditor.waitForExist();
await urlEditor.click();
await urlEditor.keys(url);
};
export const clickSendRequest = async (app: Application) => {
await app.client
.react$(COMPONENT_NAMES.requestUrlBar)
.then(e => e.$('.urlbar__send-btn'))
.then(e => e.click());
// Wait for spinner to show
const spinner = await app.client.react$('ResponseTimer');
await spinner.waitForDisplayed();
// Wait for spinner to hide
await spinner.waitForDisplayed({ reverse: true });
};
export const expect200 = async (app: Application) => {
const tag = await app.client.$('.response-pane .pane__header .tag.bg-success');
await tag.waitForDisplayed();
await expectText(tag, '200 OK');
};
export const expect401 = async (app: Application) => {
const tag = await app.client.$('.response-pane .pane__header .tag.bg-warning');
await tag.waitForDisplayed();
await expectText(tag, '401 Unauthorized');
};
export const getResponseViewer = async (app: Application) => {
// app.client.react$('ResponseViewer') doesn't seem to work because ResponseViewer is not a PureComponent
const codeEditor = await app.client.$('.response-pane .editor');
await codeEditor.waitForDisplayed();
return codeEditor;
};
export const getTimelineViewer = async (app: Application) => {
const codeEditor = await app.client.react$('ResponseTimelineViewer');
await codeEditor.waitForDisplayed();
return codeEditor;
};
export const getCsvViewer = async (app: Application) => {
const csvViewer = await app.client.react$('ResponseCSVViewer');
await csvViewer.waitForDisplayed();
return csvViewer;
};
export const getPdfCanvas = async (app: Application) => {
const pdfViewer = await app.client.react$('ResponsePDFViewer');
await pdfViewer.waitForDisplayed();
const canvas = await pdfViewer.$('.S-PDF-ID canvas');
await canvas.waitForDisplayed();
return canvas;
};
export const clickRequestAuthDropdown = async (app: Application) => {
await app.client
.react$('AuthDropdown')
.then(e => e.react$('DropdownButton'))
.then(e => e.click());
};
export const clickRequestAuthTab = async (app: Application) => {
await app.client
.react$('RequestPane')
.then(e => e.$('#react-tabs-2'))
.then(e => e.click());
};
const basicAuthPause = 300;
export const clickBasicAuth = async (app: Application) => {
await app.client
.react$('AuthDropdown')
.then(e => e.react$('DropdownItem', { props: { value: 'basic' } }))
.then(e => e.click());
// Wait for basic auth to be enabled on the request
await app.client.pause(basicAuthPause);
};
export const expectNoAuthSelected = async (app: Application) => {
const wrapper = await app.client.react$('RequestPane').then(e => e.react$('AuthWrapper'));
await wrapper.waitForDisplayed();
await expectText(wrapper, 'Select an auth type from above');
};
export const typeBasicAuthUsernameAndPassword = async (app: Application, username: string, password: string, clear = false) => {
const basicAuth = await app.client.react$('BasicAuth');
await basicAuth.waitForExist();
const usernameEditor = await app.client.react$('OneLineEditor', {
props: { id: 'username' },
});
await usernameEditor.waitForExist();
await usernameEditor.click();
// Wait for the username editor field to update
await app.client.pause(basicAuthPause);
if (clear) {
await selectAll(app);
}
await usernameEditor.keys(username);
const passwordEditor = await app.client.react$('OneLineEditor', {
props: { id: 'password' },
});
await passwordEditor.waitForExist();
await passwordEditor.click();
// Allow username changes to persist and wait for
// the password editor field to update
await app.client.pause(basicAuthPause);
if (clear) {
await selectAll(app);
}
await passwordEditor.keys(password);
// Allow password changes to persist
await app.client.pause(basicAuthPause);
};
export const toggleBasicAuthEnabled = async (app: Application) => {
await app.client
.react$('BasicAuth')
.then(e => e.$('button#enabled'))
.then(e => e.click());
// Allow toggle to persist
await app.client.pause(basicAuthPause);
};
export const toggleBasicAuthEncoding = async (app: Application) => {
await app.client
.react$('BasicAuth')
.then(e => e.$('button#use-iso-8859-1'))
.then(e => e.click());
// Allow toggle to persist
await app.client.pause(basicAuthPause);
};
export const expectText = async (element: WebdriverIO.Element, text: string) => {
await expect(element.getText()).resolves.toBe(text);
};
export const expectContainsText = async (element: WebdriverIO.Element, text: string) => {
await expect(element.getText()).resolves.toContain(text);
};
export const expectNotContainsText = async (element: WebdriverIO.Element, text: string) => {
await expect(element.getText()).resolves.not.toContain(text);
};
export const clickTimelineTab = async (app: Application) => {
await app.client
.$('.response-pane')
.then(e => e.$('#react-tabs-16'))
.then(e => e.click());
// Wait until some text shows
const codeEditor = await getTimelineViewer(app);
await app.client.waitUntil(async () => Boolean(codeEditor.getText()));
};
export const selectAll = async (app: Application) => {
await app.client.keys(spectronKeys.mapAccelerator('CommandOrControl+A'));
};
export const typeInResponseFilter = async (app: Application, filter: string) => {
const toolbar = await app.client.$('.response-pane .editor__toolbar');
await toolbar.waitForExist();
await toolbar.click();
await toolbar.keys(filter);
};

View File

@ -1,13 +0,0 @@
import { Application } from 'spectron';
export const pageDisplayed = async (app: Application) => {
await app.client.react$('WrapperDesign').then(e => e.waitForDisplayed());
};
export const goToActivity = async (app: Application, activity: 'spec' | 'debug' | 'test' = 'spec') => {
// NOTE: We shouldn't need to click the span.
// TODO: Make the radio button group in the header keyboard accessible.
const debugRadioInput = await app.client.$(`input[name="activity-toggle"][value=${activity}] ~ span`);
await debugRadioInput.click();
};

View File

@ -1,25 +0,0 @@
import findAsync from './find-async';
const findDropdownItemWithText = async (parent, text) => {
let item;
await parent.waitUntil(async () => {
const items = await parent.react$$('DropdownItem');
item = await findAsync(items, async i => (await i.getText()) === text);
return !!item;
});
return item;
};
export const clickDropdownItemByText = async (parent, text) => {
const item = await findDropdownItemWithText(parent, text);
await item.waitForDisplayed();
await item.click();
};
export const clickOpenDropdownItemByText = async (app, text) => {
const item = await app.client
.$('.dropdown__menu[aria-hidden=false]')
.then(e => e.$(`button*=${text}`));
await item.waitForDisplayed();
await item.click();
};

View File

@ -1,6 +0,0 @@
export default async function findAsync(arr, asyncCallback) {
const promises = arr.map(asyncCallback);
const results = await Promise.all(promises);
const index = results.findIndex(result => result);
return arr[index];
}

View File

@ -1,10 +0,0 @@
import fs from 'fs';
import path from 'path';
export const loadFixture = async (fixturePath: string) => {
const buffer = await fs.promises.readFile(path.join(__dirname, '..', 'fixtures', fixturePath));
const fixtureContent = buffer.toString('utf-8');
return fixtureContent;
};

View File

@ -1,97 +0,0 @@
import faker from 'faker';
import { Application } from 'spectron';
import * as debug from './debug';
import * as design from './design';
import * as dropdown from './dropdown';
import * as modal from './modal';
export const documentListingShown = async app => {
const item = await app.client.$('.document-listing');
await item.waitForExist();
};
export const expectDocumentWithTitle = async (app, title) => {
await app.client.waitUntilTextExists('.document-listing__body', title);
};
export const findCardWithTitle = async (app: Application, text: string) => {
const cards = await app.client.react$$('Card');
for (const card of cards) {
const cardTitle = await card.$('.title');
const title = await cardTitle.getText();
if (title === text) {
return card;
}
}
throw new Error(`Card with title: ${text} not found`);
};
export const cardHasBadge = async (card, text) => {
const badge = await card.$('.header-item.card-badge');
expect(await badge.getText()).toBe(text);
};
export const openDocumentWithTitle = async (app: Application, text: string) => {
const card = await findCardWithTitle(app, text);
const cardBadge = await card.$('.header-item.card-badge');
const isCollection = await cardBadge.getText() === 'Collection';
await card.waitForDisplayed();
await card.click();
if (isCollection) {
await debug.pageDisplayed(app);
} else {
await design.goToActivity(app, 'spec');
await design.pageDisplayed(app);
}
};
export const expectTotalDocuments = async (app, count) => {
const label = count > 1 ? 'Documents' : 'Document';
await app.client.waitUntilTextExists('.document-listing__footer', `${count} ${label}`);
};
export const openWorkspaceCardDropdown = async card => {
const dropdown = await card.react$('WorkspaceCardDropdown');
await dropdown.click();
};
const openCreateDropdown = async app => {
const button = await app.client.$('button*=Create');
await button.waitForClickable();
await button.click();
};
export const createNewCollection = async (app, prefix = 'coll') => {
await openCreateDropdown(app);
await dropdown.clickDropdownItemByText(app.client, 'Request Collection');
const collectionName = `${prefix}-${faker.lorem.slug()}`;
await modal.waitUntilOpened(app, { title: 'Create New Request Collection' });
await modal.typeIntoModalInput(app, collectionName);
await modal.clickModalFooterByText(app, 'Create');
return collectionName;
};
export const createNewDocument = async (app, prefix = 'doc') => {
await openCreateDropdown(app);
await dropdown.clickDropdownItemByText(app.client, 'Design Document');
const documentName = `${prefix}-${faker.lorem.slug()}`;
await modal.waitUntilOpened(app, { title: 'Create New Design Document' });
await modal.typeIntoModalInput(app, documentName);
await modal.clickModalFooterByText(app, 'Create');
return documentName;
};
export const importFromClipboard = async app => {
await openCreateDropdown(app);
await dropdown.clickDropdownItemByText(app.client, 'Clipboard');
};

View File

@ -1,50 +0,0 @@
export const migrationMessageShown = async app => {
await app.client.waitUntilTextExists(
'.onboarding__content__header h1',
'Migrate from Insomnia Designer',
);
};
export const clickSkip = async app => {
const button = await app.client.react$('MigrationBody').then(e => e.$('button=Skip for now'));
await button.waitForClickable();
await button.click();
};
export const toggleOption = async (app, label) => {
const toggle = await app.client
.$('.onboarding__content__body')
.then(e => e.react$('BooleanSetting', { props: { label } }));
await toggle.waitForClickable();
await toggle.click();
};
const _getStartButton = async app => {
return await app.client.react$('MigrationBody').then(e => e.$('button=Start Migration'));
};
export const clickStart = async app => {
const button = await _getStartButton(app);
await button.waitForClickable();
await button.click();
};
export const ensureStartNotClickable = async app => {
const button = await _getStartButton(app);
await button.waitForClickable({ reverse: true });
};
export const successMessageShown = async app => {
await app.client.waitUntilTextExists(
'.onboarding__content__body p strong',
'Migrated successfully!',
10000, // Wait 10 seconds for migration to complete
);
};
export const clickRestart = async app => {
await app.client
.react$('MigrationBody')
.then(e => e.$('button=Restart Now'))
.then(e => e.click());
};

View File

@ -1,45 +0,0 @@
import { Application } from 'spectron';
import findAsync from './find-async';
interface WaitUntilOpenedOptions {modalName?: string; title?: string}
export const waitUntilOpened = async (app, { modalName, title }: WaitUntilOpenedOptions) => {
if (modalName) {
const modal = await app.client.react$(modalName);
await modal.waitForDisplayed();
} else {
await app.client.waitUntilTextExists('div.modal__header__children', title);
}
};
export const close = async (app: Application, modalName?: string) => {
let modal;
if (modalName) {
modal = await app.client.react$(modalName);
} else {
const modals = await app.client.react$$('Modal');
modal = await findAsync(modals, m => m.isDisplayed());
}
await modal.$('button.modal__close-btn').then(e => e.click());
};
export const clickModalFooterByText = async (app, text) => {
const btn = await app.client
.$('.modal[aria-hidden=false] .modal__footer')
.then(e => e.$(`button*=${text}`));
await btn.waitForClickable();
await btn.click();
};
export const typeIntoModalInput = async (app, text) => {
const input = await app.client.$('.modal input');
await input.waitUntil(() => input.isFocused());
await input.keys(text);
};
export const selectModalOption = async (app, text) => {
const select = await app.client.$('.modal select');
await select.selectByVisibleText(text);
};

View File

@ -1,17 +0,0 @@
const analyticsMessageShown = async app => {
await app.client.waitUntilTextExists(
'p strong',
'Share Usage Analytics with Kong Inc',
);
};
const clickDontShare = async app => {
await app.client
.$('button=Don\'t share usage analytics')
.then(e => e.click());
};
export const skipOnboardingFlow = async app => {
await analyticsMessageShown(app);
await clickDontShare(app);
};

View File

@ -1,89 +0,0 @@
import { Application } from 'spectron';
import { mapAccelerator } from 'spectron-keys';
import * as dropdown from './dropdown';
import * as modal from './modal';
import { clickTabByText } from './tabs';
export const openFromSettingsButton = async (app: Application) => {
await (await app.client.$('[data-testid="settings-button"]')).click();
await modal.waitUntilOpened(app, { modalName: 'SettingsModal' });
};
export const openWithKeyboardShortcut = async (app: Application) => {
await app.client.keys(mapAccelerator('CommandOrControl+,'));
await modal.waitUntilOpened(app, { modalName: 'SettingsModal' });
};
export const closeModal = async (app: Application) => {
await modal.close(app, 'SettingsModal');
};
export const goToPluginsTab = async (app: Application) => {
// Click on the plugins tab
await app.client.react$('SettingsModal').then(e => clickTabByText(e, 'Plugins'));
// Wait for the plugins component to show
await app.client.react$('Plugins').then(e => e.waitForDisplayed());
};
export const goToDataTab = async (app: Application) => {
await app.client.react$('SettingsModal').then(e => clickTabByText(e, 'Data'));
await app.client.$('[data-testid="import-export-tab"]').then(e => e.waitForDisplayed());
};
export const importFromClipboard = async (app: Application) => {
const importExport = await app.client.$('[data-testid="import-export-tab"]');
await importExport.waitForDisplayed();
await importExport.$('button*=Import Data').then(e => e.click());
await dropdown.clickOpenDropdownItemByText(app, 'From Clipboard');
};
export const installPlugin = async (app, pluginName) => {
const plugins = await app.client.react$('SettingsModal').then(e => e.react$('Plugins'));
// Find text input and install button
const inputField = await plugins.$('form input[placeholder="npm-package-name"]');
// Click and wait for focus
await inputField.waitForEnabled();
await inputField.click();
await inputField.waitUntil(() => inputField.isFocused());
// Type plugin name
await app.client.keys(pluginName);
// Click install
const installButton = await plugins.$('button=Install Plugin');
await installButton.click();
// Button and field should disable
await plugins.waitUntil(async () => {
const buttonEnabled = await inputField.isEnabled();
const fieldEnabled = await installButton.isEnabled();
return !buttonEnabled && !fieldEnabled;
});
// Spinner should show
await installButton.$('i.fa.fa-refresh.fa-spin').then(e => e.waitForDisplayed());
// Button and field should re-enable
await plugins.waitUntil(
async () => {
const buttonEnabled = await inputField.isEnabled();
const fieldEnabled = await installButton.isEnabled();
return buttonEnabled && fieldEnabled;
},
{ timeout: 10000, timeoutMsg: 'npm was slow to install the plugin' },
);
// Plugin entry should exist in the table in the first row and second column
await app.client.waitUntilTextExists('table tr:nth-of-type(1) td:nth-of-type(2)', pluginName);
};

View File

@ -1,3 +0,0 @@
export const clickTabByText = async (element, text) => {
await element.$(`.react-tabs__tab=${text}`).then(e => e.click());
};

View File

@ -1,7 +0,0 @@
import { Application } from 'spectron';
export const waitUntilTextDisappears = async (app: Application, element: WebdriverIO.Element, text: string) => {
await app.client.waitUntil(async () =>
!(await element.getText()).includes(text)
);
};

File diff suppressed because it is too large Load Diff

View File

@ -19,16 +19,15 @@
"clean": "tsc --build tsconfig.build.json --clean",
"postclean": "rimraf dist",
"build": "tsc --build tsconfig.build.json",
"spectron:build": "cross-env BUNDLE=build xvfb-maybe jest --detectOpenHandles --testPathPattern core",
"spectron:package": "cross-env BUNDLE=package xvfb-maybe jest --detectOpenHandles --testPathPattern core",
"test:build": "xvfb-maybe cross-env BUNDLE=build playwright test",
"test:package": "xvfb-maybe cross-env BUNDLE=package playwright test",
"cli": "jest --detectOpenHandles --testPathPattern cli",
"serve": "ts-node server/index.ts",
"with-mock": "concurrently --names server,app --success first --kill-others \"npm run serve\"",
"test:cli": "npm run with-mock \"npm run cli\"",
"test:build": "npm run with-mock \"npm run spectron:build\"",
"test:package": "npm run with-mock \"npm run spectron:package\""
"test:cli": "npm run with-mock \"npm run cli\""
},
"devDependencies": {
"@playwright/test": "^1.16.3",
"@types/concurrently": "^6.0.1",
"@types/express": "^4.17.11",
"@types/faker": "^5.5.5",
@ -45,9 +44,8 @@
"mkdirp": "^1.0.4",
"ramda": "^0.27.1",
"ramda-adjunct": "^2.34.0",
"spectron": "^13.0.0",
"spectron-keys": "0.0.1",
"ts-node": "^9.1.1",
"uuid": "^8.3.0",
"xvfb-maybe": "^0.2.1"
}
}

View File

@ -0,0 +1,15 @@
/* eslint-disable filenames/match-exported */
import { PlaywrightTestConfig } from '@playwright/test';
const config: PlaywrightTestConfig = {
webServer: {
command: 'npm run serve',
port: 4010,
timeout: 120 * 1000,
reuseExistingServer: !process.env.CI,
},
timeout: process.env.CI ? 60 * 1000 : 20 * 1000,
forbidOnly: !!process.env.CI,
outputDir: 'screenshots',
testDir: 'tests',
};
export default config;

View File

@ -0,0 +1,45 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { exit } from 'process';
import * as uuid from 'uuid';
export const loadFixture = async (fixturePath: string) => {
const buffer = await fs.promises.readFile(path.join(__dirname, '..', 'fixtures', fixturePath));
return buffer.toString('utf-8');
};
export const randomDataPath = () => path.join(os.tmpdir(), 'insomnia-smoke-test', `${uuid.v4()}`);
export const DESIGNER_DATA_PATH = path.join(__dirname, '..', 'fixtures', 'basic-designer');
const pathLookup = {
win32: path.join('win-unpacked', 'Insomnia.exe'),
darwin: path.join('mac', 'Insomnia.app', 'Contents', 'MacOS', 'Insomnia'),
linux: path.join('linux-unpacked', 'insomnia'),
};
const insomniaBinary = path.join('dist', pathLookup[process.platform]);
const electronBinary = path.join('node_modules', '.bin', process.platform === 'win32' ? 'electron.cmd' : 'electron');
export const executablePath = process.env.BUNDLE === 'package' ? insomniaBinary : electronBinary;
export const mainPath = path.join('build', 'main.min.js');
export const cwd = path.resolve(__dirname, '..', '..', 'insomnia-app');
const hasMainBeenBuilt = fs.existsSync(path.resolve(cwd, mainPath));
const hasBinaryBeenBuilt = fs.existsSync(path.resolve(cwd, insomniaBinary));
// NOTE: guard against missing build artifacts
if (process.env.BUNDLE !== 'package' && !hasMainBeenBuilt) {
console.error(`ERROR: ${mainPath} not found at ${path.resolve(cwd, mainPath)}
Have you run "npm run app-build:smoke"?`);
exit(1);
}
if (process.env.BUNDLE === 'package' && !hasBinaryBeenBuilt) {
console.error(`ERROR: ${insomniaBinary} not found at ${path.resolve(cwd, insomniaBinary)}
Have you run "npm run app-package:smoke"?`);
exit(1);
}
if (process.env.DEBUG) {
console.log(`Using current working directory at ${cwd}`);
console.log(`Using executablePath at ${executablePath}`);
console.log(`Using mainPath at ${mainPath}`);
}

View File

@ -0,0 +1,80 @@
import { PlaywrightWorkerArgs, test } from '@playwright/test';
import { cwd, DESIGNER_DATA_PATH, executablePath, loadFixture, mainPath, randomDataPath } from '../playwright/paths';
// NOTE: the DESIGNER_DATA_PATH argument is only used for overriding paths for migration testing,
// if we remove migration from insomnia designer support this testing flow can be simplifed.
type Playwright = PlaywrightWorkerArgs['playwright'];
interface EnvOptions { INSOMNIA_DATA_PATH: string; DESIGNER_DATA_PATH?: string }
const newPage = async ({ playwright, options }: ({ playwright: Playwright; options: EnvOptions })) => {
// NOTE: ensure the DESIGNER_DATA_PATH is ignored from process.env
if (!options.DESIGNER_DATA_PATH) options.DESIGNER_DATA_PATH = 'doesnt-exist';
const electronApp = await playwright._electron.launch({
cwd,
executablePath,
args: process.env.BUNDLE === 'package' ? [] : [mainPath],
env: {
...process.env,
...options,
PLAYWRIGHT: 'true',
},
});
await electronApp.waitForEvent('window');
const page = await electronApp.firstWindow();
// @TODO: Investigate why the app doesn't start without a reload with playwright in windows
if (process.platform === 'win32') await page.reload();
return { electronApp, page };
};
test('can send requests', async ({ playwright }) => {
const options = { INSOMNIA_DATA_PATH: randomDataPath() };
const { page, electronApp } = await newPage({ playwright, options });
await page.click('text=Don\'t share usage analytics');
await page.click('text=Create');
const text = await loadFixture('smoke-test-collection.yaml');
await electronApp.evaluate(async ({ clipboard }, text) => clipboard.writeText(text), text);
await page.click('button:has-text("Clipboard")');
await page.click('text=CollectionSmoke testsjust now');
await page.click('button:has-text("GETsend JSON request")');
await page.click('text=http://127.0.0.1:4010/pets/1Send >> button');
await page.click('text=200 OK');
await page.click('button:has-text("GETsends dummy.csv request and shows rich response")');
await page.click('text=http://127.0.0.1:4010/file/dummy.csvSend >> button');
await page.click('text=200 OK');
await page.click('button:has-text("GETsends dummy.xml request and shows raw response")');
await page.click('text=http://127.0.0.1:4010/file/dummy.xmlSend >> button');
await page.click('text=200 OK');
await page.click('button:has-text("GETsends dummy.pdf request and shows rich response")');
await page.click('text=http://127.0.0.1:4010/file/dummy.pdfSend >> button');
await page.click('text=200 OK');
await page.click('button:has-text("GETsends request with basic authentication")');
await page.click('text=http://127.0.0.1:4010/auth/basicSend >> button');
await page.click('text=200 OK');
await page.close();
});
test.describe.serial('given a designer and data directory', () => {
const INSOMNIA_DATA_PATH = randomDataPath();
test('should complete migration dialog', async ({ playwright }) => {
const options = { DESIGNER_DATA_PATH, INSOMNIA_DATA_PATH };
const { page } = await newPage({ playwright, options });
await page.click('text=Copy Workspaces');
await page.click('text=Copy Plugins');
await page.click('text=Copy Designer Application Settings');
await page.click('text=Start Migration');
await page.click('text=Migrated successfully!');
await page.close();
});
test('then on restart should see the migrated workspace', async ({ playwright }) => {
const options = { DESIGNER_DATA_PATH, INSOMNIA_DATA_PATH };
const { page } = await newPage({ playwright, options });
await page.click('text=Don\'t share usage analytics');
await page.click('text=BASIC-DESIGNER-FIXTURE');
await page.close();
});
});

View File

@ -6,7 +6,6 @@
"rootDir": ".",
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true, // this is required because spectron depends on electron but it is not locatable by typescript for the purpose of types
"noImplicitAny": false
},
"include": [

View File

@ -6,11 +6,11 @@
},
"include": [
"__jest__",
"tests",
"cli",
"core",
"designer",
"fixtures",
"modules",
"playwright.config.ts",
"playwright",
"server",
"jest.config.js",
".eslintrc.js"