mirror of
https://github.com/Kong/insomnia.git
synced 2026-11-04 20:13:48 -05:00
Add tests and enums for handling undiscardable changes in Git sync
- Introduced `GitChangeList` enum to categorize changes as Staged or Unstaged. - Implemented multiple test cases to verify the behavior of undiscardable changes: - Committing a reordered file leaves the working tree clean. - Confirming discard on a reordered file brings the change back. - Mixed changes (real edits with reorders) are not flagged until the real edit is discarded. - Non-Insomnia added and deleted files show no undiscardable warning. - Files from older schemas are flagged as undiscardable. - Real edits show no undiscardable warning in diffs or discard confirmations. - Staged hand-reordered files show no undiscardable warning. - Warnings are consistent in both diffs and discard confirmations for hand-reordered files.
This commit is contained in:
14 files changed
+636
-169
No files matched your search
@@ -127,7 +127,7 @@ Skip this step when nothing about the real app's live behavior is in question.
|
||||
- This step only applies once step 2 has concluded the scenario needs a genuinely new file — don't re-decide placement for something you're extending in place.
|
||||
- Main path (create → act → assert, the "everything works" case): `tests/<domain>/happy-path.spec.ts`. If one already exists for that domain, do not overwrite it with a different scenario — add a new, separately named file instead (a same-intent extension belongs under step 2, not here).
|
||||
- Edge case / error scenario: `tests/<domain>/<scenario-described-in-kebab-case>.spec.ts` — name the file after the scenario itself (e.g. `unlinked-environment-variable.spec.ts`), never `test1.spec.ts` or similar.
|
||||
- **A family of related specs for one feature/ticket** (several edge-case files that all drive the same scripted mock server, e.g. one per case of a test plan) may live in a subdirectory of the domain (`tests/mcp-client/error-response/`) — one spec file per case, named after the scenario. Shared setup (a `connectTo(user, config)`-style helper, canned payloads, expected-title helpers) goes in a non-spec `helpers.ts` next to them (it isn't matched by the `*.spec.ts` test pattern); adjust relative imports by one extra `../`. Don't use this for a single spec — that stays a plain file under `tests/<domain>/`.
|
||||
- **A family of related specs for one feature/ticket** (several edge-case files that all drive the same scripted mock server, e.g. one per case of a test plan) may live in a subdirectory of the domain (`tests/mcp-client/error-response/`, `tests/git-sync/undiscardable-change/`) — one spec file per case, named after the scenario. Shared setup (a `connectTo(user, config)`-style helper, canned payloads, expected-title helpers) goes in a non-spec `helpers.ts` next to them (it isn't matched by the `*.spec.ts` test pattern); adjust relative imports by one extra `../`. Don't use this for a single spec — that stays a plain file under `tests/<domain>/`.
|
||||
- `tests/data/import/` and `tests/data/export/` sit together under `tests/data/` since both drive Preferences -> Data; `tests/data/import/` is further split by source: `file/`, `url/`, `curl/`, `clipboard/`, `mcp/`.
|
||||
- **If the test needs a fixture file** (a JSON/YAML/WSDL/etc. imported or read via `path.join(__dirname, "...")`), give the spec its own directory named after the spec file (without `.spec.ts`), and put the spec and every fixture it uses inside that directory together — e.g. `tests/data/import/file/import-open-api/import-open-api.spec.ts` + `tests/data/import/file/import-open-api/httpbingo-get.json`. Adjust the relative import depth to `misc/fixtures`/`enums/`/`models/` by one extra `../` versus a plain domain-level spec, since the file now sits one directory deeper. A spec with no fixture stays a plain file directly under `tests/<domain>/` (or `tests/data/import/<source>/`) — don't create a directory for it.
|
||||
|
||||
|
||||
@@ -1232,6 +1232,7 @@ getChanges(): Promise<{ staged: string[]; unstaged: string[] }> // file names (
|
||||
stageChanges(fileNames: string[]): Promise<void> // stages each file individually from the Commit modal; fileNames as listed, e.g. "foo.yaml"
|
||||
unstageChanges(fileNames: string[]): Promise<void> // unstages each file individually
|
||||
discardAllChanges(): Promise<void> // discards every uncommitted local change
|
||||
discardChangeAfterReview(fileName: string): Promise<void> // opens the file's diff, clicks Discard, confirms, closes the modal — does NOT wait for the row to leave the unstaged list, so it works for a change that comes straight back (see "Undiscardable change" below)
|
||||
createBranch(branch: string): Promise<void> // creates + checks out a new local branch
|
||||
createBranchExpectingError(branch: string): Promise<string> // submits a duplicate/invalid branch name, returns the validation error text under "New branch name", closes the modal
|
||||
getBranches(): Promise<{ local: string[]; remote: string[] }> // local + remote-only branch lists from the Branches modal, then closes it
|
||||
@@ -1246,6 +1247,29 @@ deleteBranch(branch: string, switchToBranch = "master"): Promise<void> // check
|
||||
|
||||
**Direct `gitSyncPage` calls with no Flow wrapper** (the real specs use these — destructure `gitSyncPage` from `user.pageManager`): `openCommitDialog()`/`closeCommitDialog()`/`isCommitDialogOpen()`, `commitStaged(message?)` (fills the Message field, **clearing it when omitted** since it keeps its text between attempts), `getStagedChanges()`/`getUnstagedChanges()`, `stageChange(fileName)`/`unstageChange(fileName)`, `openHistory()`/`getLatestCommit()`/`closeHistory()`, `getCurrentBranch()`. The "Stage All Changes" prompt — shown when Commit is clicked with **nothing staged** — has `getStageAllPromptText()` (waits for it; text contains "There are no staged changes to commit"), `cancelStageAllPrompt()` (leaves the modal open, nothing committed), and `confirmStageAllPrompt()` ("Yes": stages everything + commits, waits for the modal to close). `isCommitDisabled()` was removed — the Commit button is no longer disabled with nothing staged, it opens this prompt instead. `openCommitDialog()` now also waits for at least one change row, so reading staged/unstaged right after it can't see a still-empty list.
|
||||
|
||||
**Undiscardable-change warning (INS-3830) — direct `gitSyncPage` calls, no Flow wrapper** (specs in `tests/git-sync/undiscardable-change/`, a family directory with a shared `helpers.ts`). All of these need `openCommitDialog()` first and leave the dialog open unless noted; `closeCommitDialog()` is only needed before the next `openCommitDialog()`/menu use, not at the end of a test:
|
||||
|
||||
```ts
|
||||
// enums/git-change-list.ts — GitChangeList.Staged = "Staged changes" | GitChangeList.Unstaged = "Unstaged changes"
|
||||
openChangeDiff(list: GitChangeList, fileName: string): Promise<void> // clicks the row and waits for the diff pane (Stage/Unstage this file button); the warning flag arrives with the diff, so once this resolves its presence/absence is settled
|
||||
isUndiscardableWarningVisibleInDiff(): Promise<boolean> // "This change is only Insomnia internally reformatting the file…" banner above the diff
|
||||
openDiscardConfirmation(fileName: string): Promise<void> // clicks Discard on an unstaged row, waits for the "Discard Changes" dialog, does not confirm
|
||||
getDiscardConfirmationText(): Promise<string> // the dialog's text
|
||||
isUndiscardableWarningVisibleInDiscardDialog(): Promise<boolean>
|
||||
confirmDiscard(): Promise<void> // confirms without waiting for the row to disappear (unlike discardChange(), which does)
|
||||
cancelDiscard(): Promise<void>
|
||||
hasUncommittedChanges(): Promise<boolean> // reads the Git Sync menu's Commit item (disabled = clean tree); opens/closes the menu itself, no Commit dialog needed. Poll it — it lags the watcher
|
||||
```
|
||||
|
||||
Always pass `GitChangeList.Staged`/`.Unstaged` (never the raw "Staged changes" string) to `openChangeDiff`, and note `getChanges()`/`openCommitDialog()` **throw/time out on a clean tree** (the Commit item is disabled), so never use them to assert "nothing changed" — use `hasUncommittedChanges()`.
|
||||
|
||||
**Facts learned building these specs:**
|
||||
- The Commit modal only lists **`.yaml`** files (`git-vcs.ts` filters on extension). A "non-Insomnia file" must be a non-Insomnia `.yaml` (e.g. `openapi: 3.0.0…`), never README.md.
|
||||
- Seed a remote file **before** creating the Git project: `pushServerFile({ path, content })` then `workspaceFlow.create(new Project(…, ProjectType.Git), GIT_CREDENTIAL.name)` — the clone imports it and the watcher re-exports it, so a non-canonical file shows up as an unstaged change with no `pull()` needed.
|
||||
- A hand-reordered / old-schema Insomnia file is built by `buildReorderedCollectionFile(name, { oldSchema? })` in `tests/git-sync/undiscardable-change/helpers.ts` (folders listed against their `created` order; `oldSchema` drops `schema_version` and adds a header `id`). Copy that helper's shape for any other "file Insomnia rewrites" fixture; `buildCollectionFile()` from git-fixtures is already canonical-ish and won't trigger it.
|
||||
- To modify a file on disk (e.g. a non-Insomnia yaml), pass `{ cloneParentDir: fs.mkdtempSync(path.join(os.tmpdir(), "…")) }` as the 3rd arg of the Git `workspaceFlow.create()`; the clone is the single sub-directory inside it (`path.join(dir, fs.readdirSync(dir)[0])`). Added/deleted files are easiest via the UI (`workspaceFlow.create(project, collection, fileName)` / `workspaceFlow.delete(collection)`).
|
||||
- After a discard of an undiscardable file the change reappears asynchronously — poll `getChanges().unstaged` for it rather than reading once.
|
||||
|
||||
`Commit` (`models/commit.ts`): `{ message: string; id?: string }` — positional constructor (`new Commit(message)`), `id` populated after `commit()`/`commitAndPush()` read it back from the History modal's row `data-key` (the real git OID).
|
||||
|
||||
**Creating a Git Sync project and its collections** goes through `workspaceFlow.create()`'s dedicated overloads, not a separate flow:
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export enum GitChangeList {
|
||||
Staged = "Staged changes",
|
||||
Unstaged = "Unstaged changes",
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { GitChangeList } from "../enums/git-change-list";
|
||||
import { Commit } from "../models/commit";
|
||||
import { BaseFlow } from "./base.flow";
|
||||
|
||||
@@ -127,6 +128,21 @@ export class GitSyncFlow extends BaseFlow {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a file's diff and discards it, confirming the dialog, without
|
||||
* waiting for the file to leave the unstaged list (so it also works for a
|
||||
* change that comes straight back), then closes the modal.
|
||||
* @param fileName - The file name as listed in the modal, e.g. "foo.yaml"
|
||||
*/
|
||||
async discardChangeAfterReview(fileName: string): Promise<void> {
|
||||
const { gitSyncPage } = this.pageManager;
|
||||
await gitSyncPage.openCommitDialog();
|
||||
await gitSyncPage.openChangeDiff(GitChangeList.Unstaged, fileName);
|
||||
await gitSyncPage.openDiscardConfirmation(fileName);
|
||||
await gitSyncPage.confirmDiscard();
|
||||
await gitSyncPage.closeCommitDialog();
|
||||
}
|
||||
|
||||
/**
|
||||
* Discards every uncommitted local change.
|
||||
*/
|
||||
|
||||
@@ -1,35 +1,38 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { expect } from '@playwright/test';
|
||||
|
||||
import { DEFAULT_TIMEOUT } from "../misc/constants";
|
||||
import { BasePage } from "./base.page";
|
||||
import { GitChangeList } from '../enums/git-change-list';
|
||||
import { DEFAULT_TIMEOUT } from '../misc/constants';
|
||||
import { BasePage } from './base.page';
|
||||
|
||||
export class GitSyncPage extends BasePage {
|
||||
private readonly syncDropdownButton = this.page.getByTestId("git-dropdown");
|
||||
private readonly syncMenu = this.page.getByRole("menu", {
|
||||
name: "Git Sync",
|
||||
private readonly syncDropdownButton = this.page.getByTestId('git-dropdown');
|
||||
private readonly syncMenu = this.page.getByRole('menu', {
|
||||
name: 'Git Sync',
|
||||
});
|
||||
private readonly commitDialog = this.page.getByRole("dialog", {
|
||||
name: "Commit Changes",
|
||||
private readonly commitDialog = this.page.getByRole('dialog', {
|
||||
name: 'Commit Changes',
|
||||
});
|
||||
private readonly branchesDialog = this.page.getByRole("dialog", {
|
||||
name: "Branches",
|
||||
private readonly branchesDialog = this.page.getByRole('dialog', {
|
||||
name: 'Branches',
|
||||
});
|
||||
private readonly stageAllPrompt = this.page
|
||||
.getByRole("dialog")
|
||||
.filter({ hasText: "Stage All Changes" });
|
||||
private readonly mergeConflictsDialog = this.page.getByRole("dialog", {
|
||||
name: "Resolve conflicts",
|
||||
private readonly stageAllPrompt = this.page.getByRole('dialog').filter({ hasText: 'Stage All Changes' });
|
||||
private readonly mergeConflictsDialog = this.page.getByRole('dialog', {
|
||||
name: 'Resolve conflicts',
|
||||
});
|
||||
private readonly historyDialog = this.page
|
||||
.getByRole("dialog")
|
||||
.filter({ has: this.page.getByRole("heading", { name: "History" }) });
|
||||
.getByRole('dialog')
|
||||
.filter({ has: this.page.getByRole('heading', { name: 'History' }) });
|
||||
|
||||
private readonly changeRow = (
|
||||
list: "Staged changes" | "Unstaged changes",
|
||||
fileName: string,
|
||||
) =>
|
||||
private readonly discardDialog = this.page
|
||||
.getByRole('dialog')
|
||||
.filter({ has: this.page.getByRole('heading', { name: 'Discard Changes' }) });
|
||||
private readonly undiscardableWarning = this.page.getByText(
|
||||
'This change is only Insomnia internally reformatting the file',
|
||||
);
|
||||
|
||||
private readonly changeRow = (list: GitChangeList, fileName: string) =>
|
||||
this.commitDialog
|
||||
.getByRole("grid", { name: list, exact: true })
|
||||
.getByRole('grid', { name: list, exact: true })
|
||||
.locator('[role="row"][data-key]')
|
||||
.filter({ hasText: fileName });
|
||||
|
||||
@@ -43,9 +46,9 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async navigate(): Promise<void> {
|
||||
await this.page
|
||||
.getByText("Setup Git Credentials")
|
||||
.getByText('Setup Git Credentials')
|
||||
.or(this.syncDropdownButton)
|
||||
.waitFor({ state: "visible", timeout: DEFAULT_TIMEOUT });
|
||||
.waitFor({ state: 'visible', timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,9 +68,7 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async switchBranch(branch: string): Promise<void> {
|
||||
await this.openSyncMenu();
|
||||
await this.syncMenu
|
||||
.getByRole("menuitemradio", { name: branch, exact: true })
|
||||
.click();
|
||||
await this.syncMenu.getByRole('menuitemradio', { name: branch, exact: true }).click();
|
||||
await expect(this.syncDropdownButton).toContainText(branch, {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
@@ -78,9 +79,7 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async fetch(): Promise<void> {
|
||||
await this.openSyncMenu();
|
||||
await this.syncMenu
|
||||
.getByRole("menuitemradio", { name: "Fetch", exact: true })
|
||||
.click();
|
||||
await this.syncMenu.getByRole('menuitemradio', { name: 'Fetch', exact: true }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,9 +89,7 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async pull(): Promise<void> {
|
||||
await this.openSyncMenu();
|
||||
await this.syncMenu
|
||||
.getByRole("menuitemradio", { name: "Pull", exact: true })
|
||||
.click();
|
||||
await this.syncMenu.getByRole('menuitemradio', { name: 'Pull', exact: true }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,10 +98,8 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async push(): Promise<void> {
|
||||
await this.openSyncMenu();
|
||||
await this.syncMenu
|
||||
.getByRole("menuitemradio", { name: "Push", exact: true })
|
||||
.click();
|
||||
await expect(this.page.getByText("Push completed")).toBeVisible({
|
||||
await this.syncMenu.getByRole('menuitemradio', { name: 'Push', exact: true }).click();
|
||||
await expect(this.page.getByText('Push completed')).toBeVisible({
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
}
|
||||
@@ -126,29 +121,23 @@ export class GitSyncPage extends BasePage {
|
||||
async openCommitDialog(): Promise<void> {
|
||||
await expect(async () => {
|
||||
await this.openSyncMenu();
|
||||
const commitItem = this.syncMenu.getByRole("menuitemradio", {
|
||||
name: "Commit",
|
||||
const commitItem = this.syncMenu.getByRole('menuitemradio', {
|
||||
name: 'Commit',
|
||||
exact: true,
|
||||
});
|
||||
try {
|
||||
await expect(commitItem).toBeEnabled({ timeout: 3000 });
|
||||
} catch (error) {
|
||||
await this.page.keyboard.press("Escape");
|
||||
await this.page.keyboard.press('Escape');
|
||||
throw error;
|
||||
}
|
||||
await commitItem.click();
|
||||
}).toPass({ timeout: DEFAULT_TIMEOUT * 3 });
|
||||
await expect(this.commitDialog).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
await expect(this.commitDialog).not.toHaveAttribute(
|
||||
"data-loading",
|
||||
"true",
|
||||
{
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
},
|
||||
);
|
||||
await expect(
|
||||
this.commitDialog.locator('[role="row"][data-key]').first(),
|
||||
).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
await expect(this.commitDialog).not.toHaveAttribute('data-loading', 'true', {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
await expect(this.commitDialog.locator('[role="row"][data-key]').first()).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -163,14 +152,14 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
private async stageAllChanges(): Promise<void> {
|
||||
await this.commitDialog
|
||||
.getByRole("grid", { name: "Unstaged changes" })
|
||||
.getByRole("row")
|
||||
.getByRole('grid', { name: 'Unstaged changes' })
|
||||
.getByRole('row')
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: DEFAULT_TIMEOUT });
|
||||
.waitFor({ state: 'visible', timeout: DEFAULT_TIMEOUT });
|
||||
await this.commitDialog.locator('button[name="Stage all changes"]').click();
|
||||
await expect(
|
||||
this.commitDialog.getByRole("heading", { name: /^Staged changes [1-9]/ }),
|
||||
).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
await expect(this.commitDialog.getByRole('heading', { name: /^Staged changes [1-9]/ })).toBeVisible({
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -180,12 +169,8 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async commit(message: string): Promise<void> {
|
||||
await this.stageAllChanges();
|
||||
await this.commitDialog
|
||||
.getByRole("textbox", { name: "Message" })
|
||||
.fill(message);
|
||||
await this.commitDialog
|
||||
.getByRole("button", { name: "Commit", exact: true })
|
||||
.click();
|
||||
await this.commitDialog.getByRole('textbox', { name: 'Message' }).fill(message);
|
||||
await this.commitDialog.getByRole('button', { name: 'Commit', exact: true }).click();
|
||||
await expect(this.commitDialog).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
@@ -200,16 +185,10 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async commitAndPush(message: string): Promise<void> {
|
||||
await this.stageAllChanges();
|
||||
await this.commitDialog
|
||||
.getByRole("textbox", { name: "Message" })
|
||||
.fill(message);
|
||||
await this.commitDialog
|
||||
.getByRole("button", { name: "Commit and push" })
|
||||
.click();
|
||||
await this.commitDialog.getByRole('textbox', { name: 'Message' }).fill(message);
|
||||
await this.commitDialog.getByRole('button', { name: 'Commit and push' }).click();
|
||||
await expect(this.commitDialog).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
await expect(
|
||||
this.page.getByText("Changes committed and pushed"),
|
||||
).toBeVisible({
|
||||
await expect(this.page.getByText('Changes committed and pushed')).toBeVisible({
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
}
|
||||
@@ -236,7 +215,7 @@ export class GitSyncPage extends BasePage {
|
||||
* @returns The staged file names
|
||||
*/
|
||||
async getStagedChanges(): Promise<string[]> {
|
||||
return this.readChangeRows("Staged changes");
|
||||
return this.readChangeRows(GitChangeList.Staged);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -245,7 +224,7 @@ export class GitSyncPage extends BasePage {
|
||||
* @returns The unstaged file names
|
||||
*/
|
||||
async getUnstagedChanges(): Promise<string[]> {
|
||||
return this.readChangeRows("Unstaged changes");
|
||||
return this.readChangeRows(GitChangeList.Unstaged);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,10 +234,10 @@ export class GitSyncPage extends BasePage {
|
||||
* @param fileName - The file name as listed in the modal, e.g. "foo.yaml"
|
||||
*/
|
||||
async stageChange(fileName: string): Promise<void> {
|
||||
const row = this.changeRow("Unstaged changes", fileName);
|
||||
const row = this.changeRow(GitChangeList.Unstaged, fileName);
|
||||
await row.hover();
|
||||
await row.locator('button[name="Stage change"]').click();
|
||||
await expect(this.changeRow("Staged changes", fileName)).toBeVisible({
|
||||
await expect(this.changeRow(GitChangeList.Staged, fileName)).toBeVisible({
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
}
|
||||
@@ -270,10 +249,10 @@ export class GitSyncPage extends BasePage {
|
||||
* @param fileName - The file name as listed in the modal, e.g. "foo.yaml"
|
||||
*/
|
||||
async unstageChange(fileName: string): Promise<void> {
|
||||
const row = this.changeRow("Staged changes", fileName);
|
||||
const row = this.changeRow(GitChangeList.Staged, fileName);
|
||||
await row.hover();
|
||||
await row.locator('button[name="Unstage change"]').click();
|
||||
await expect(this.changeRow("Unstaged changes", fileName)).toBeVisible({
|
||||
await expect(this.changeRow(GitChangeList.Unstaged, fileName)).toBeVisible({
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
}
|
||||
@@ -285,9 +264,9 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async unstageAllChanges(): Promise<void> {
|
||||
await this.commitDialog.locator('button[name="Unstage all changes"]').click();
|
||||
await expect(
|
||||
this.commitDialog.getByRole("heading", { name: /^Staged changes 0/ }),
|
||||
).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
await expect(this.commitDialog.getByRole('heading', { name: /^Staged changes 0/ })).toBeVisible({
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -297,13 +276,99 @@ export class GitSyncPage extends BasePage {
|
||||
* @param fileName - The file name as listed in the modal, e.g. "foo.yaml"
|
||||
*/
|
||||
async discardChange(fileName: string): Promise<void> {
|
||||
const row = this.changeRow("Unstaged changes", fileName);
|
||||
const row = this.changeRow(GitChangeList.Unstaged, fileName);
|
||||
await row.hover();
|
||||
await row.locator('button[name="Discard change"]').click();
|
||||
await this.page.getByTestId("discard-changes-confirm-button").click();
|
||||
await this.page.getByTestId('discard-changes-confirm-button').click();
|
||||
await expect(row).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks a file's row in the open Commit modal to open its diff, and
|
||||
* waits for the diff pane (its Stage/Unstage this-file button) to render
|
||||
* — the undiscardable flag arrives with the same loader data as the
|
||||
* diff, so once the pane is up the warning's presence is settled.
|
||||
* Requires `openCommitDialog()` to have been called first.
|
||||
* @param list - Which list the file is in
|
||||
* @param fileName - The file name as listed in the modal, e.g. "foo.yaml"
|
||||
*/
|
||||
async openChangeDiff(list: GitChangeList, fileName: string): Promise<void> {
|
||||
await this.changeRow(list, fileName).click();
|
||||
await expect(
|
||||
this.commitDialog.getByRole('button', {
|
||||
name: list === GitChangeList.Staged ? 'Unstage this file' : 'Stage this file',
|
||||
}),
|
||||
).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads whether the open Commit modal's diff pane shows the "can't be
|
||||
* discarded" warning. Requires `openChangeDiff()` to have been called.
|
||||
* @returns True if the warning banner is visible in the diff pane
|
||||
*/
|
||||
async isUndiscardableWarningVisible(): Promise<boolean> {
|
||||
return this.commitDialog.getByText('This change is only Insomnia internally reformatting the file').isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks Discard on a file's row and waits for the confirmation dialog,
|
||||
* without confirming. Requires `openCommitDialog()` to have been called.
|
||||
* @param fileName - The file name as listed in the modal, e.g. "foo.yaml"
|
||||
*/
|
||||
async openDiscardConfirmation(fileName: string): Promise<void> {
|
||||
const row = this.changeRow(GitChangeList.Unstaged, fileName);
|
||||
await row.hover();
|
||||
await row.locator('button[name="Discard change"]').click();
|
||||
await expect(this.discardDialog).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the open Discard confirmation dialog's body text.
|
||||
* @returns The dialog's text content
|
||||
*/
|
||||
async getDiscardConfirmationText(): Promise<string> {
|
||||
return (await this.discardDialog.innerText()).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads whether the open Discard confirmation dialog shows the "can't be
|
||||
* discarded" warning.
|
||||
* @returns True if the warning is visible in the dialog
|
||||
*/
|
||||
async isUndiscardableWarningVisibleInDiscardDialog(): Promise<boolean> {
|
||||
return this.discardDialog.getByText('This change is only Insomnia internally reformatting the file').isVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirms the open Discard dialog without waiting for the file to leave
|
||||
* the unstaged list (a file whose change can't be discarded comes back).
|
||||
*/
|
||||
async confirmDiscard(): Promise<void> {
|
||||
await this.page.getByTestId('discard-changes-confirm-button').click();
|
||||
await expect(this.discardDialog).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the open Discard confirmation dialog.
|
||||
*/
|
||||
async cancelDiscard(): Promise<void> {
|
||||
await this.discardDialog.getByRole('button', { name: 'Cancel' }).click();
|
||||
await expect(this.discardDialog).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads whether the Git Sync menu's "Commit" item is enabled, i.e.
|
||||
* whether anything is uncommitted. Opens and closes the menu.
|
||||
* @returns True if there are uncommitted changes
|
||||
*/
|
||||
async hasUncommittedChanges(): Promise<boolean> {
|
||||
await this.openSyncMenu();
|
||||
const enabled = await this.syncMenu.getByRole('menuitemradio', { name: 'Commit', exact: true }).isEnabled();
|
||||
await this.page.keyboard.press('Escape');
|
||||
await expect(this.syncMenu).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills `message` (clearing the field when omitted, since it keeps its
|
||||
* previous text between attempts) and clicks "Commit" on whatever is
|
||||
@@ -314,12 +379,8 @@ export class GitSyncPage extends BasePage {
|
||||
* @param message - The commit message; omit to submit with an empty message
|
||||
*/
|
||||
async commitStaged(message?: string): Promise<void> {
|
||||
await this.commitDialog
|
||||
.getByRole("textbox", { name: "Message" })
|
||||
.fill(message ?? "");
|
||||
await this.commitDialog
|
||||
.getByRole("button", { name: "Commit", exact: true })
|
||||
.click();
|
||||
await this.commitDialog.getByRole('textbox', { name: 'Message' }).fill(message ?? '');
|
||||
await this.commitDialog.getByRole('button', { name: 'Commit', exact: true }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -337,7 +398,7 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async getStageAllPromptText(): Promise<string> {
|
||||
await expect(this.stageAllPrompt).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
return (await this.stageAllPrompt.innerText()).replaceAll("\n", " ");
|
||||
return (await this.stageAllPrompt.innerText()).replaceAll('\n', ' ');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,7 +406,7 @@ export class GitSyncPage extends BasePage {
|
||||
* Commit modal open and nothing committed.
|
||||
*/
|
||||
async cancelStageAllPrompt(): Promise<void> {
|
||||
await this.stageAllPrompt.getByRole("button", { name: "Cancel" }).click();
|
||||
await this.stageAllPrompt.getByRole('button', { name: 'Cancel' }).click();
|
||||
await expect(this.stageAllPrompt).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
@@ -354,7 +415,7 @@ export class GitSyncPage extends BasePage {
|
||||
* change and commits it, then waits for the Commit modal to close.
|
||||
*/
|
||||
async confirmStageAllPrompt(): Promise<void> {
|
||||
await this.stageAllPrompt.getByRole("button", { name: "Yes" }).click();
|
||||
await this.stageAllPrompt.getByRole('button', { name: 'Yes' }).click();
|
||||
await expect(this.commitDialog).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
@@ -362,9 +423,9 @@ export class GitSyncPage extends BasePage {
|
||||
* Waits until the Commit modal's staged list holds no files.
|
||||
*/
|
||||
async waitForNothingStaged(): Promise<void> {
|
||||
await expect(
|
||||
this.commitDialog.getByRole("heading", { name: /^Staged changes 0/ }),
|
||||
).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
await expect(this.commitDialog.getByRole('heading', { name: /^Staged changes 0/ })).toBeVisible({
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -375,14 +436,12 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async discardAllChanges(): Promise<void> {
|
||||
await this.commitDialog
|
||||
.getByRole("grid", { name: "Unstaged changes" })
|
||||
.getByRole("row")
|
||||
.getByRole('grid', { name: 'Unstaged changes' })
|
||||
.getByRole('row')
|
||||
.first()
|
||||
.waitFor({ state: "visible", timeout: DEFAULT_TIMEOUT });
|
||||
await this.commitDialog
|
||||
.locator('button[name="Discard all changes"]')
|
||||
.click();
|
||||
await this.page.getByTestId("discard-changes-confirm-button").click();
|
||||
.waitFor({ state: 'visible', timeout: DEFAULT_TIMEOUT });
|
||||
await this.commitDialog.locator('button[name="Discard all changes"]').click();
|
||||
await this.page.getByTestId('discard-changes-confirm-button').click();
|
||||
await expect(this.commitDialog).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
@@ -397,7 +456,7 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async openHistory(): Promise<void> {
|
||||
await this.openSyncMenu();
|
||||
await this.syncMenu.getByText("History", { exact: true }).click();
|
||||
await this.syncMenu.getByText('History', { exact: true }).click();
|
||||
await expect(this.historyDialog).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
@@ -417,18 +476,16 @@ export class GitSyncPage extends BasePage {
|
||||
* for the log to finish loading.
|
||||
* @returns The latest commit's id and message, or undefined if the log is empty
|
||||
*/
|
||||
async getLatestCommit(): Promise<
|
||||
{ id: string; message: string } | undefined
|
||||
> {
|
||||
async getLatestCommit(): Promise<{ id: string; message: string } | undefined> {
|
||||
const row = this.historyDialog.locator('[role="row"][data-key]').first();
|
||||
try {
|
||||
await expect(row).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
const id = await row.getAttribute("data-key");
|
||||
const id = await row.getAttribute('data-key');
|
||||
if (!id) return undefined;
|
||||
const message = await row.getByRole("rowheader").innerText();
|
||||
const message = await row.getByRole('rowheader').innerText();
|
||||
return { id, message };
|
||||
}
|
||||
|
||||
@@ -436,7 +493,7 @@ export class GitSyncPage extends BasePage {
|
||||
* Closes the History modal via Escape.
|
||||
*/
|
||||
async closeHistory(): Promise<void> {
|
||||
await this.page.keyboard.press("Escape");
|
||||
await this.page.keyboard.press('Escape');
|
||||
await expect(this.historyDialog).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
@@ -445,9 +502,7 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async openBranchesDialog(): Promise<void> {
|
||||
await this.openSyncMenu();
|
||||
await this.syncMenu
|
||||
.getByRole("menuitemradio", { name: "Branches", exact: true })
|
||||
.click();
|
||||
await this.syncMenu.getByRole('menuitemradio', { name: 'Branches', exact: true }).click();
|
||||
await expect(this.branchesDialog).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
@@ -460,12 +515,8 @@ export class GitSyncPage extends BasePage {
|
||||
* @param branch - The new branch's name
|
||||
*/
|
||||
async createBranch(branch: string): Promise<void> {
|
||||
await this.branchesDialog
|
||||
.getByRole("textbox", { name: "New branch name:" })
|
||||
.fill(branch);
|
||||
await this.branchesDialog
|
||||
.getByRole("button", { name: "Create", exact: true })
|
||||
.click();
|
||||
await this.branchesDialog.getByRole('textbox', { name: 'New branch name:' }).fill(branch);
|
||||
await this.branchesDialog.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
await expect(this.branchesDialog.getByText(`${branch} *`)).toBeVisible({
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
@@ -478,8 +529,8 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async checkoutBranch(branch: string): Promise<void> {
|
||||
await this.branchesDialog
|
||||
.getByRole("row", { name: branch, exact: true })
|
||||
.getByRole("button", { name: "Checkout" })
|
||||
.getByRole('row', { name: branch, exact: true })
|
||||
.getByRole('button', { name: 'Checkout' })
|
||||
.click();
|
||||
}
|
||||
|
||||
@@ -489,12 +540,12 @@ export class GitSyncPage extends BasePage {
|
||||
* @param branch - The branch to delete
|
||||
*/
|
||||
async deleteBranch(branch: string): Promise<void> {
|
||||
const row = this.branchesDialog.getByRole("row", {
|
||||
const row = this.branchesDialog.getByRole('row', {
|
||||
name: branch,
|
||||
exact: true,
|
||||
});
|
||||
await row.getByRole("button", { name: "Delete" }).click();
|
||||
await row.getByRole("button", { name: "Confirm" }).click();
|
||||
await row.getByRole('button', { name: 'Delete' }).click();
|
||||
await row.getByRole('button', { name: 'Confirm' }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,11 +554,8 @@ export class GitSyncPage extends BasePage {
|
||||
* @param branch - The branch to merge in
|
||||
*/
|
||||
async mergeBranch(branch: string): Promise<void> {
|
||||
await this.branchesDialog
|
||||
.getByLabel(branch, { exact: true })
|
||||
.getByRole("button", { name: "Merge" })
|
||||
.click();
|
||||
await this.page.getByRole("button", { name: "Confirm" }).click();
|
||||
await this.branchesDialog.getByLabel(branch, { exact: true }).getByRole('button', { name: 'Merge' }).click();
|
||||
await this.page.getByRole('button', { name: 'Confirm' }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -518,12 +566,8 @@ export class GitSyncPage extends BasePage {
|
||||
* @param branch - The branch name to attempt to create
|
||||
*/
|
||||
async submitNewBranch(branch: string): Promise<void> {
|
||||
await this.branchesDialog
|
||||
.getByRole("textbox", { name: "New branch name:" })
|
||||
.fill(branch);
|
||||
await this.branchesDialog
|
||||
.getByRole("button", { name: "Create", exact: true })
|
||||
.click();
|
||||
await this.branchesDialog.getByRole('textbox', { name: 'New branch name:' }).fill(branch);
|
||||
await this.branchesDialog.getByRole('button', { name: 'Create', exact: true }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -533,7 +577,7 @@ export class GitSyncPage extends BasePage {
|
||||
* @returns The error text
|
||||
*/
|
||||
async getNewBranchError(): Promise<string> {
|
||||
const error = this.branchesDialog.locator("form p");
|
||||
const error = this.branchesDialog.locator('form p');
|
||||
await expect(error).toBeVisible({ timeout: DEFAULT_TIMEOUT });
|
||||
return (await error.innerText()).trim();
|
||||
}
|
||||
@@ -545,7 +589,7 @@ export class GitSyncPage extends BasePage {
|
||||
* @returns The local branch names
|
||||
*/
|
||||
async getLocalBranches(): Promise<string[]> {
|
||||
return this.readBranchRows("Branches list");
|
||||
return this.readBranchRows('Branches list');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -555,10 +599,8 @@ export class GitSyncPage extends BasePage {
|
||||
* @returns The remote-only branch names
|
||||
*/
|
||||
async getRemoteBranches(): Promise<string[]> {
|
||||
await expect(
|
||||
this.branchesDialog.getByText("Fetching remote branches..."),
|
||||
).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
return this.readBranchRows("Remote Branches list");
|
||||
await expect(this.branchesDialog.getByText('Fetching remote branches...')).toBeHidden({ timeout: DEFAULT_TIMEOUT });
|
||||
return this.readBranchRows('Remote Branches list');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -569,10 +611,10 @@ export class GitSyncPage extends BasePage {
|
||||
*/
|
||||
async checkoutRemoteBranch(branch: string): Promise<void> {
|
||||
await this.branchesDialog
|
||||
.getByRole("grid", { name: "Remote Branches list", exact: true })
|
||||
.getByRole("row")
|
||||
.getByRole('grid', { name: 'Remote Branches list', exact: true })
|
||||
.getByRole('row')
|
||||
.filter({ hasText: branch })
|
||||
.getByRole("button", { name: "Fetch and checkout" })
|
||||
.getByRole('button', { name: 'Fetch and checkout' })
|
||||
.click();
|
||||
await expect(this.branchesDialog.getByText(`${branch} *`)).toBeVisible({
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
@@ -586,26 +628,22 @@ export class GitSyncPage extends BasePage {
|
||||
* close. Call after `mergeBranch()` or `pull()` hit conflicts.
|
||||
* @param version - "Current" keeps the local change, "Incoming" takes the merged-in one
|
||||
*/
|
||||
async resolveMergeConflicts(version: "Current" | "Incoming"): Promise<void> {
|
||||
async resolveMergeConflicts(version: 'Current' | 'Incoming'): Promise<void> {
|
||||
await expect(this.mergeConflictsDialog).toBeVisible({
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
const rows = this.mergeConflictsDialog
|
||||
.getByRole("grid", { name: "Conflicted changes" })
|
||||
.getByRole("row");
|
||||
const rows = this.mergeConflictsDialog.getByRole('grid', { name: 'Conflicted changes' }).getByRole('row');
|
||||
const count = await rows.count();
|
||||
for (let i = 0; i < count; i++) {
|
||||
await rows.nth(i).click();
|
||||
await this.mergeConflictsDialog
|
||||
.getByRole("button", {
|
||||
.getByRole('button', {
|
||||
name: `Take all ${version.toLowerCase()} changes`,
|
||||
})
|
||||
.click();
|
||||
await this.page.getByRole("button", { name: "Confirm" }).click();
|
||||
await this.page.getByRole('button', { name: 'Confirm' }).click();
|
||||
}
|
||||
await this.mergeConflictsDialog
|
||||
.getByRole("button", { name: "Resolve conflicts" })
|
||||
.click();
|
||||
await this.mergeConflictsDialog.getByRole('button', { name: 'Resolve conflicts' }).click();
|
||||
await expect(this.mergeConflictsDialog).toBeHidden({
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
});
|
||||
@@ -615,30 +653,27 @@ export class GitSyncPage extends BasePage {
|
||||
* Closes the Branches modal.
|
||||
*/
|
||||
async closeBranchesDialog(): Promise<void> {
|
||||
await this.page.getByTestId("close-git-project-branches-modal").click();
|
||||
await this.page.getByTestId('close-git-project-branches-modal').click();
|
||||
await this.page
|
||||
.getByTestId("git-project-branches-modal-overlay")
|
||||
.waitFor({ state: "hidden", timeout: DEFAULT_TIMEOUT });
|
||||
.getByTestId('git-project-branches-modal-overlay')
|
||||
.waitFor({ state: 'hidden', timeout: DEFAULT_TIMEOUT });
|
||||
}
|
||||
|
||||
private async readChangeRows(
|
||||
list: "Staged changes" | "Unstaged changes",
|
||||
): Promise<string[]> {
|
||||
const rows = this.commitDialog
|
||||
.getByRole("grid", { name: list, exact: true })
|
||||
.locator('[role="row"][data-key]');
|
||||
private async readChangeRows(list: GitChangeList): Promise<string[]> {
|
||||
const rows = this.commitDialog.getByRole('grid', { name: list, exact: true }).locator('[role="row"][data-key]');
|
||||
const texts = await rows.allInnerTexts();
|
||||
return texts.map((text) => text.split("\n")[0].trim());
|
||||
return texts.map(text => text.split('\n')[0].trim());
|
||||
}
|
||||
|
||||
private async readBranchRows(
|
||||
list: "Branches list" | "Remote Branches list",
|
||||
): Promise<string[]> {
|
||||
const rows = this.branchesDialog
|
||||
.getByRole("grid", { name: list, exact: true })
|
||||
.locator('[role="row"][data-key]');
|
||||
private async readBranchRows(list: 'Branches list' | 'Remote Branches list'): Promise<string[]> {
|
||||
const rows = this.branchesDialog.getByRole('grid', { name: list, exact: true }).locator('[role="row"][data-key]');
|
||||
const texts = await rows.allInnerTexts();
|
||||
return texts.map((text) => text.split("\n")[0].replace(/\s*\*$/, "").trim());
|
||||
return texts.map(text =>
|
||||
text
|
||||
.split('\n')[0]
|
||||
.replace(/\s*\*$/, '')
|
||||
.trim(),
|
||||
);
|
||||
}
|
||||
|
||||
private async openSyncMenu(): Promise<void> {
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
import { ProjectType } from "../../../enums/project-types";
|
||||
import {
|
||||
DEFAULT_TIMEOUT,
|
||||
expect,
|
||||
GIT_CREDENTIAL,
|
||||
pushServerFile,
|
||||
test,
|
||||
} from "../../../misc/git-fixtures";
|
||||
import { Project } from "../../../models/project";
|
||||
import { buildReorderedCollectionFile } from "./helpers";
|
||||
|
||||
test("Verify committing a hand-reordered file once leaves the working tree clean", async ({
|
||||
user,
|
||||
}) => {
|
||||
const { gitSyncFlow, preferencesFlow, workspaceFlow } = user.flowManager;
|
||||
const { gitSyncPage } = user.pageManager;
|
||||
|
||||
const fileName = `${faker.string.alpha(10).toLowerCase()}.yaml`;
|
||||
await pushServerFile({
|
||||
path: fileName,
|
||||
content: buildReorderedCollectionFile(faker.string.alpha(10)).content,
|
||||
});
|
||||
await preferencesFlow.addGitCredential(GIT_CREDENTIAL);
|
||||
await workspaceFlow.create(
|
||||
new Project(faker.string.alphanumeric(10), ProjectType.Git),
|
||||
GIT_CREDENTIAL.name,
|
||||
);
|
||||
await expect
|
||||
.poll(async () => (await gitSyncFlow.getChanges()).unstaged, {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
.toContain(fileName);
|
||||
const message = faker.string.alphanumeric(10);
|
||||
const commit = await gitSyncFlow.commit(message);
|
||||
|
||||
await expect
|
||||
.poll(async () => gitSyncPage.hasUncommittedChanges(), {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
.toBe(false);
|
||||
expect(commit.message).toBe(message);
|
||||
});
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { GitChangeList } from '../../../enums/git-change-list';
|
||||
import { ProjectType } from '../../../enums/project-types';
|
||||
import { DEFAULT_TIMEOUT, expect, GIT_CREDENTIAL, pushServerFile, test } from '../../../misc/git-fixtures';
|
||||
import { Project } from '../../../models/project';
|
||||
import { buildReorderedCollectionFile } from './helpers';
|
||||
|
||||
test('Verify confirming discard on a hand-reordered file brings the same change straight back', async ({ user }) => {
|
||||
const { gitSyncFlow, preferencesFlow, workspaceFlow } = user.flowManager;
|
||||
const { gitSyncPage } = user.pageManager;
|
||||
|
||||
const fileName = `${faker.string.alpha(10).toLowerCase()}.yaml`;
|
||||
await pushServerFile({
|
||||
path: fileName,
|
||||
content: buildReorderedCollectionFile(faker.string.alpha(10)).content,
|
||||
});
|
||||
await preferencesFlow.addGitCredential(GIT_CREDENTIAL);
|
||||
await workspaceFlow.create(new Project(faker.string.alphanumeric(10), ProjectType.Git), GIT_CREDENTIAL.name);
|
||||
await expect
|
||||
.poll(async () => (await gitSyncFlow.getChanges()).unstaged, {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
.toContain(fileName);
|
||||
await gitSyncFlow.discardChangeAfterReview(fileName);
|
||||
|
||||
await expect
|
||||
.poll(async () => (await gitSyncFlow.getChanges()).unstaged, {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
.toContain(fileName);
|
||||
await gitSyncPage.openCommitDialog();
|
||||
await gitSyncPage.openChangeDiff(GitChangeList.Unstaged, fileName);
|
||||
expect(await gitSyncPage.isUndiscardableWarningVisible()).toBe(true);
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
import { faker } from "@faker-js/faker";
|
||||
|
||||
export const UNDISCARDABLE_WARNING_TEXT =
|
||||
"This change is only Insomnia internally reformatting the file";
|
||||
|
||||
export function buildReorderedCollectionFile(
|
||||
collectionName: string,
|
||||
options: { oldSchema?: boolean } = {},
|
||||
) {
|
||||
const id = faker.string.alphanumeric({ length: 16, casing: "lower" });
|
||||
const folders = [
|
||||
{ id: `fld_b${id}`, name: faker.string.alpha(10), created: 3000 },
|
||||
{ id: `fld_a${id}`, name: faker.string.alpha(10), created: 2000 },
|
||||
];
|
||||
const content = [
|
||||
"type: collection.insomnia.rest/5.0",
|
||||
...(options.oldSchema ? [] : ['schema_version: "5.1"']),
|
||||
`name: ${collectionName}`,
|
||||
"meta:",
|
||||
` id: wrk_${id}`,
|
||||
" created: 1000",
|
||||
" modified: 1000",
|
||||
"collection:",
|
||||
...folders.flatMap((folder) => [
|
||||
` - name: ${folder.name}`,
|
||||
" meta:",
|
||||
` id: ${folder.id}`,
|
||||
` created: ${folder.created}`,
|
||||
` modified: ${folder.created}`,
|
||||
" children:",
|
||||
` - url: https://example.com/${folder.id}`,
|
||||
` name: Request in ${folder.name}`,
|
||||
" meta:",
|
||||
` id: req_${folder.id}`,
|
||||
" created: 5000",
|
||||
" modified: 5000",
|
||||
" method: GET",
|
||||
" headers:",
|
||||
" - name: X-Folder",
|
||||
` value: ${folder.id}`,
|
||||
...(options.oldSchema ? [" id: pair_1"] : []),
|
||||
]),
|
||||
"cookieJar:",
|
||||
" name: Default Jar",
|
||||
" meta:",
|
||||
` id: jar_${id}`,
|
||||
" created: 1000",
|
||||
" modified: 1000",
|
||||
"environments:",
|
||||
" name: Base Environment",
|
||||
" meta:",
|
||||
` id: env_${id}`,
|
||||
" created: 1000",
|
||||
" modified: 1000",
|
||||
"",
|
||||
].join("\n");
|
||||
return { content, requestName: `Request in ${folders[0].name}` };
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { GitChangeList } from '../../../enums/git-change-list';
|
||||
import { ProjectType } from '../../../enums/project-types';
|
||||
import { DEFAULT_TIMEOUT, expect, GIT_CREDENTIAL, pushServerFile, test } from '../../../misc/git-fixtures';
|
||||
import { Collection } from '../../../models/collection';
|
||||
import { Project } from '../../../models/project';
|
||||
import { buildReorderedCollectionFile } from './helpers';
|
||||
|
||||
test('Verify a real edit mixed with a reorder is not flagged until the real edit is discarded', async ({ user }) => {
|
||||
const { gitSyncFlow, httpRequestFlow, preferencesFlow, workspaceFlow } = user.flowManager;
|
||||
const { gitSyncPage } = user.pageManager;
|
||||
|
||||
const fileName = `${faker.string.alpha(10).toLowerCase()}.yaml`;
|
||||
const collectionName = faker.string.alpha(10);
|
||||
const { content, requestName } = buildReorderedCollectionFile(collectionName);
|
||||
await pushServerFile({ path: fileName, content });
|
||||
await preferencesFlow.addGitCredential(GIT_CREDENTIAL);
|
||||
await workspaceFlow.create(new Project(faker.string.alphanumeric(10), ProjectType.Git), GIT_CREDENTIAL.name);
|
||||
const collection = new Collection(collectionName);
|
||||
const request = await httpRequestFlow.get(requestName, collection);
|
||||
await workspaceFlow.rename(request!, faker.string.alphanumeric(10));
|
||||
await expect
|
||||
.poll(async () => (await gitSyncFlow.getChanges()).unstaged, {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
.toContain(fileName);
|
||||
await gitSyncPage.openCommitDialog();
|
||||
await gitSyncPage.openChangeDiff(GitChangeList.Unstaged, fileName);
|
||||
const warningWithMixedChange = await gitSyncPage.isUndiscardableWarningVisible();
|
||||
await gitSyncPage.closeCommitDialog();
|
||||
await gitSyncFlow.discardChangeAfterReview(fileName);
|
||||
|
||||
expect(warningWithMixedChange).toBe(false);
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
await gitSyncPage.openCommitDialog();
|
||||
await gitSyncPage.openChangeDiff(GitChangeList.Unstaged, fileName);
|
||||
const hasWarning = await gitSyncPage.isUndiscardableWarningVisible();
|
||||
await gitSyncPage.closeCommitDialog();
|
||||
return hasWarning;
|
||||
},
|
||||
{ timeout: DEFAULT_TIMEOUT },
|
||||
)
|
||||
.toBe(true);
|
||||
});
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { GitChangeList } from '../../../enums/git-change-list';
|
||||
import { ProjectType } from '../../../enums/project-types';
|
||||
import { DEFAULT_TIMEOUT, expect, GIT_CREDENTIAL, pushServerFile, test } from '../../../misc/git-fixtures';
|
||||
import { Collection } from '../../../models/collection';
|
||||
import { Project } from '../../../models/project';
|
||||
|
||||
test('Verify a modified non-Insomnia file and an added or deleted file show no undiscardable warning', async ({
|
||||
user,
|
||||
}) => {
|
||||
const { gitSyncFlow, preferencesFlow, workspaceFlow } = user.flowManager;
|
||||
const { gitSyncPage } = user.pageManager;
|
||||
|
||||
const otherFileName = `${faker.string.alpha(10).toLowerCase()}.yaml`;
|
||||
await pushServerFile({ path: otherFileName, content: `openapi: 3.0.0\ninfo:\n title: ${faker.string.alpha(10)}\n` });
|
||||
await preferencesFlow.addGitCredential(GIT_CREDENTIAL);
|
||||
const cloneParentDir = fs.mkdtempSync(path.join(os.tmpdir(), 'insomnia-undiscardable-'));
|
||||
const project = await workspaceFlow.create(
|
||||
new Project(faker.string.alphanumeric(10), ProjectType.Git),
|
||||
GIT_CREDENTIAL.name,
|
||||
{ cloneParentDir },
|
||||
);
|
||||
const deletedFile = faker.string.alpha(10).toLowerCase();
|
||||
const deletedCollection = await workspaceFlow.create(
|
||||
project,
|
||||
new Collection(faker.string.alphanumeric(10)),
|
||||
deletedFile,
|
||||
);
|
||||
await gitSyncFlow.commit(faker.string.alphanumeric(10));
|
||||
const cloneDir = path.join(cloneParentDir, fs.readdirSync(cloneParentDir)[0]);
|
||||
fs.appendFileSync(path.join(cloneDir, otherFileName), ` version: ${faker.string.numeric(3)}\n`);
|
||||
const addedFile = faker.string.alpha(11).toLowerCase();
|
||||
await workspaceFlow.create(project, new Collection(faker.string.alphanumeric(10)), addedFile);
|
||||
await workspaceFlow.delete(deletedCollection);
|
||||
await expect
|
||||
.poll(async () => (await gitSyncFlow.getChanges()).unstaged.length, {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
.toBeGreaterThanOrEqual(3);
|
||||
const unstaged = (await gitSyncFlow.getChanges()).unstaged;
|
||||
const warnings = [];
|
||||
for (const file of unstaged) {
|
||||
await gitSyncPage.openCommitDialog();
|
||||
await gitSyncPage.openChangeDiff(GitChangeList.Unstaged, file);
|
||||
warnings.push(await gitSyncPage.isUndiscardableWarningVisible());
|
||||
await gitSyncPage.closeCommitDialog();
|
||||
}
|
||||
|
||||
expect(unstaged).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining(otherFileName),
|
||||
expect.stringContaining(addedFile),
|
||||
expect.stringContaining(deletedFile),
|
||||
]),
|
||||
);
|
||||
expect(warnings).toEqual(unstaged.map(() => false));
|
||||
});
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { GitChangeList } from '../../../enums/git-change-list';
|
||||
import { ProjectType } from '../../../enums/project-types';
|
||||
import { DEFAULT_TIMEOUT, expect, GIT_CREDENTIAL, pushServerFile, test } from '../../../misc/git-fixtures';
|
||||
import { Project } from '../../../models/project';
|
||||
import { buildReorderedCollectionFile } from './helpers';
|
||||
|
||||
test('Verify a file from an older schema that Insomnia only normalizes is flagged as undiscardable', async ({
|
||||
user,
|
||||
}) => {
|
||||
const { gitSyncFlow, preferencesFlow, workspaceFlow } = user.flowManager;
|
||||
const { gitSyncPage } = user.pageManager;
|
||||
|
||||
const fileName = `${faker.string.alpha(10).toLowerCase()}.yaml`;
|
||||
await pushServerFile({
|
||||
path: fileName,
|
||||
content: buildReorderedCollectionFile(faker.string.alpha(10), {
|
||||
oldSchema: true,
|
||||
}).content,
|
||||
});
|
||||
await preferencesFlow.addGitCredential(GIT_CREDENTIAL);
|
||||
await workspaceFlow.create(new Project(faker.string.alphanumeric(10), ProjectType.Git), GIT_CREDENTIAL.name);
|
||||
await expect
|
||||
.poll(async () => (await gitSyncFlow.getChanges()).unstaged, {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
.toContain(fileName);
|
||||
|
||||
await gitSyncPage.openCommitDialog();
|
||||
await gitSyncPage.openChangeDiff(GitChangeList.Unstaged, fileName);
|
||||
|
||||
expect(await gitSyncPage.isUndiscardableWarningVisible()).toBe(true);
|
||||
});
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { GitChangeList } from '../../../enums/git-change-list';
|
||||
import { HttpMethod } from '../../../enums/http-method';
|
||||
import { ProjectType } from '../../../enums/project-types';
|
||||
import { HTTP_SERVER } from '../../../misc/constants';
|
||||
import { DEFAULT_TIMEOUT, expect, GIT_CREDENTIAL, test } from '../../../misc/git-fixtures';
|
||||
import { Collection } from '../../../models/collection';
|
||||
import { Project } from '../../../models/project';
|
||||
|
||||
test('Verify a real edit shows no undiscardable warning in the diff or the discard confirmation', async ({ user }) => {
|
||||
const { gitSyncFlow, httpRequestFlow, preferencesFlow, workspaceFlow } = user.flowManager;
|
||||
const { gitSyncPage } = user.pageManager;
|
||||
|
||||
await preferencesFlow.addGitCredential(GIT_CREDENTIAL);
|
||||
const project = await workspaceFlow.create(
|
||||
new Project(faker.string.alphanumeric(10), ProjectType.Git),
|
||||
GIT_CREDENTIAL.name,
|
||||
);
|
||||
const fileName = faker.string.alpha(10).toLowerCase();
|
||||
const collection = await workspaceFlow.create(project, new Collection(faker.string.alphanumeric(10)), fileName);
|
||||
const request = await httpRequestFlow.create(collection, {
|
||||
name: faker.string.alphanumeric(10),
|
||||
method: HttpMethod.Get,
|
||||
url: `${HTTP_SERVER}/cookies`,
|
||||
});
|
||||
await gitSyncFlow.commit(faker.string.alphanumeric(10));
|
||||
await workspaceFlow.rename(request, faker.string.alphanumeric(10));
|
||||
await expect
|
||||
.poll(async () => (await gitSyncFlow.getChanges()).unstaged.length, {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
.toBeGreaterThan(0);
|
||||
const unstaged = (await gitSyncFlow.getChanges()).unstaged[0];
|
||||
|
||||
await gitSyncPage.openCommitDialog();
|
||||
await gitSyncPage.openChangeDiff(GitChangeList.Unstaged, unstaged);
|
||||
expect(await gitSyncPage.isUndiscardableWarningVisible()).toBe(false);
|
||||
|
||||
await gitSyncPage.openDiscardConfirmation(unstaged);
|
||||
expect(await gitSyncPage.isUndiscardableWarningVisibleInDiscardDialog()).toBe(false);
|
||||
});
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { GitChangeList } from '../../../enums/git-change-list';
|
||||
import { ProjectType } from '../../../enums/project-types';
|
||||
import { DEFAULT_TIMEOUT, expect, GIT_CREDENTIAL, pushServerFile, test } from '../../../misc/git-fixtures';
|
||||
import { Project } from '../../../models/project';
|
||||
import { buildReorderedCollectionFile } from './helpers';
|
||||
|
||||
test('Verify a staged hand-reordered file shows no undiscardable warning', async ({ user }) => {
|
||||
const { gitSyncFlow, preferencesFlow, workspaceFlow } = user.flowManager;
|
||||
const { gitSyncPage } = user.pageManager;
|
||||
|
||||
const fileName = `${faker.string.alpha(10).toLowerCase()}.yaml`;
|
||||
await pushServerFile({
|
||||
path: fileName,
|
||||
content: buildReorderedCollectionFile(faker.string.alpha(10)).content,
|
||||
});
|
||||
await preferencesFlow.addGitCredential(GIT_CREDENTIAL);
|
||||
await workspaceFlow.create(new Project(faker.string.alphanumeric(10), ProjectType.Git), GIT_CREDENTIAL.name);
|
||||
await expect
|
||||
.poll(async () => (await gitSyncFlow.getChanges()).unstaged, {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
.toContain(fileName);
|
||||
await gitSyncFlow.stageChanges([fileName]);
|
||||
|
||||
await gitSyncPage.openCommitDialog();
|
||||
await gitSyncPage.openChangeDiff(GitChangeList.Staged, fileName);
|
||||
expect(await gitSyncPage.isUndiscardableWarningVisible()).toBe(false);
|
||||
});
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
import { GitChangeList } from '../../../enums/git-change-list';
|
||||
import { ProjectType } from '../../../enums/project-types';
|
||||
import { DEFAULT_TIMEOUT, expect, GIT_CREDENTIAL, pushServerFile, test } from '../../../misc/git-fixtures';
|
||||
import { Project } from '../../../models/project';
|
||||
import { buildReorderedCollectionFile, UNDISCARDABLE_WARNING_TEXT } from './helpers';
|
||||
|
||||
test('Verify a hand-reordered file warns that the change cannot be discarded in both the diff and the discard confirmation', async ({
|
||||
user,
|
||||
}) => {
|
||||
const { gitSyncFlow, preferencesFlow, workspaceFlow } = user.flowManager;
|
||||
const { gitSyncPage } = user.pageManager;
|
||||
|
||||
const fileName = `${faker.string.alpha(10).toLowerCase()}.yaml`;
|
||||
await pushServerFile({
|
||||
path: fileName,
|
||||
content: buildReorderedCollectionFile(faker.string.alpha(10)).content,
|
||||
});
|
||||
await preferencesFlow.addGitCredential(GIT_CREDENTIAL);
|
||||
await workspaceFlow.create(new Project(faker.string.alphanumeric(10), ProjectType.Git), GIT_CREDENTIAL.name);
|
||||
await expect
|
||||
.poll(async () => (await gitSyncFlow.getChanges()).unstaged, {
|
||||
timeout: DEFAULT_TIMEOUT,
|
||||
})
|
||||
.toContain(fileName);
|
||||
|
||||
await gitSyncPage.openCommitDialog();
|
||||
await gitSyncPage.openChangeDiff(GitChangeList.Unstaged, fileName);
|
||||
const hasDiffWarning = await gitSyncPage.isUndiscardableWarningVisible();
|
||||
await gitSyncPage.openDiscardConfirmation(fileName);
|
||||
|
||||
expect(hasDiffWarning).toBe(true);
|
||||
expect(await gitSyncPage.isUndiscardableWarningVisibleInDiscardDialog()).toBe(true);
|
||||
expect(await gitSyncPage.getDiscardConfirmationText()).toContain(UNDISCARDABLE_WARNING_TEXT);
|
||||
});
|
||||
Reference in new issue
Block a user