cleanup integration test code

This commit is contained in:
Jesse Duffield 2023-02-26 11:49:15 +11:00
parent 8b5d59c238
commit f7e8b2dd71
70 changed files with 322 additions and 272 deletions

2
.gitignore vendored
View file

@ -35,7 +35,7 @@ lazygit.exe
test/git_server/data
test/integration/**
test/results/**
oryxBuildBinary
__debug_bin

View file

@ -3,8 +3,8 @@ package hosting_service
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/fakes"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
)
@ -328,7 +328,7 @@ func TestGetPullRequestURL(t *testing.T) {
s := s
t.Run(s.testName, func(t *testing.T) {
tr := i18n.EnglishTranslationSet()
log := &test.FakeFieldLogger{}
log := &fakes.FakeFieldLogger{}
hostingServiceMgr := NewHostingServiceMgr(log, &tr, s.remoteUrl, s.configServiceDomains)
s.test(hostingServiceMgr.GetPullRequestURL(s.from, s.to))
log.AssertErrors(t, s.expectedLoggedErrors)

View file

@ -1,4 +1,4 @@
package test
package fakes
import (
"fmt"

View file

@ -16,7 +16,7 @@ go run cmd/integration_test/main.go cli [--slow or --sandbox] [testname or testp
## Writing tests
The tests live in pkg/integration/tests. Each test is listed in `pkg/integration/tests/tests_gen.go` which is an auto-generated file. You can re-generate that file by running `go generate ./...` at the root of the Lazygit repo.
The tests live in pkg/integration/tests. Each test is registered in `pkg/integration/tests/test_list.go` which is an auto-generated file. You can re-generate that file by running `go generate ./...` at the root of the Lazygit repo.
Each test has two important steps: the setup step and the run step.
@ -34,18 +34,6 @@ The run step has two arguments passed in:
`t` is for driving the gui by pressing certain keys, selecting list items, etc.
`keys` is for use when getting the test to press a particular key e.g. `t.Views().Commits().Focus().PressKey(keys.Universal.Confirm)`
### Tips
#### Handle most setup in the `shell` part of the test
Try to do as much setup work as possible in your setup step. For example, if all you're testing is that the user is able to resolve merge conflicts, create the merge conflicts in the setup step. On the other hand, if you're testing to see that lazygit can warn the user about merge conflicts after an attempted merge, it's fine to wait until the run step to actually create the conflicts. If the run step is focused on the thing you're trying to test, the test will run faster and its intent will be clearer.
#### Create helper functions for (very) frequently used test logic
If you find yourself doing something frequently in a test, consider making it a method in one of the helper arguments. For example, instead of calling `t.PressKey(keys.Universal.Confirm)` in 100 places, it's better to have a method `t.Confirm()`. This is not to say that everything should be made into a helper method: just things that are particularly common in tests.
Also, given how often we need to select a menu item or type into a prompt panel, there are some helper functions for that. See `ExpectConfirmation` for an example.
## Running tests
There are three ways to invoke a test:
@ -62,7 +50,7 @@ The name of a test is based on its path, so the name of the test at `pkg/integra
You can pass the KEY_PRESS_DELAY env var to the test runner in order to set a delay in milliseconds between keypresses, which helps for watching a test at a realistic speed to understand what it's doing. Or you can pass the '--slow' flag which sets a pre-set 'slow' key delay. In the tui you can press 't' to run the test in slow mode.
The resultant repo will be stored in `test/integration`, so if you're not sure what went wrong you can go there and inspect the repo.
The resultant repo will be stored in `test/results`, so if you're not sure what went wrong you can go there and inspect the repo.
### Running tests in VSCode
@ -78,3 +66,19 @@ The test will run in a VSCode terminal:
Say you want to do a manual test of how lazygit handles merge-conflicts, but you can't be bothered actually finding a way to create merge conflicts in a repo. To make your life easier, you can simply run a merge-conflicts test in sandbox mode, meaning the setup step is run for you, and then instead of the test driving the lazygit session, you're allowed to drive it yourself.
To run a test in sandbox mode you can press 's' on a test in the test TUI or in the test runner pass the --sandbox argument.
## Tips for writing tests
### Handle most setup in the `shell` part of the test
Try to do as much setup work as possible in your setup step. For example, if all you're testing is that the user is able to resolve merge conflicts, create the merge conflicts in the setup step. On the other hand, if you're testing to see that lazygit can warn the user about merge conflicts after an attempted merge, it's fine to wait until the run step to actually create the conflicts. If the run step is focused on the thing you're trying to test, the test will run faster and its intent will be clearer.
### Create helper functions for (very) frequently used test logic
If within a test directory you find several tests need to share some logic, you can create a file called `shared.go` in that directory to hold shared helper functions (see `pkg/integration/tests/filter_by_path/shared.go` for an example).
If you need to share test logic across test directories you can put helper functions in the `tests/shared` package. If you find yourself frequently doing the same thing from within a test across test directories, for example, responding a particular popup, consider adding a helper method to `pkg/integration/components/common.go`. If you look around the code in the `components` directory you may find another place that's sensible to put your helper function.
### Don't do too much in one test
If you're testing different pieces of functionality, it's better to test them in isolation using multiple short tests, compared to one larger longer test. Sometimes it's appropriate to have a longer test which tests how various different pieces interact, but err on the side of keeping things short.

View file

@ -11,6 +11,7 @@ import (
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests"
"github.com/samber/lo"
)
// see pkg/integration/README.md
@ -65,6 +66,12 @@ func getTestsToRun(testNames []string) []*components.IntegrationTest {
)
})
if lo.SomeBy(testNames, func(name string) bool {
return strings.HasSuffix(name, "/shared")
}) {
log.Fatalf("'shared' is a reserved name for tests that are shared between multiple test files. Please rename your test.")
}
outer:
for _, testName := range testNames {
// check if our given test name actually exists
@ -74,7 +81,7 @@ outer:
continue outer
}
}
log.Fatalf("test %s not found. Perhaps you forgot to add it to `pkg/integration/integration_tests/tests_gen.go`? This can be done by running `go generate ./...` from the Lazygit root. You'll need to ensure that your test name and the file name match (where the test name is in PascalCase and the file name is in snake_case).", testName)
log.Fatalf("test %s not found. Perhaps you forgot to add it to `pkg/integration/integration_tests/test_list.go`? This can be done by running `go generate ./...` from the Lazygit root. You'll need to ensure that your test name and the file name match (where the test name is in PascalCase and the file name is in snake_case).", testName)
}
return testsToRun

View file

@ -137,7 +137,7 @@ func RunTUI() {
return nil
}
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("code test/integration/%s", currentTest.Name()))
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("code test/results/%s", currentTest.Name()))
if err := cmd.Run(); err != nil {
return err
}

View file

@ -18,7 +18,7 @@ func retryWaitTimes() []int {
// give it more leeway compared to when we're running things locally.
return []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200, 500, 1000, 2000, 4000}
} else {
return []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100}
return []int{0, 1, 1, 1, 1, 1, 5, 10, 20, 40, 100, 200}
}
}

View file

@ -1,12 +1,12 @@
package components
// for running common actions
type Actions struct {
type Common struct {
t *TestDriver
}
func (self *Actions) ContinueMerge() {
self.t.Views().current().Press(self.t.keys.Universal.CreateRebaseOptionsMenu)
func (self *Common) ContinueMerge() {
self.t.GlobalPress(self.t.keys.Universal.CreateRebaseOptionsMenu)
self.t.ExpectPopup().Menu().
Title(Equals("Rebase Options")).
@ -14,32 +14,32 @@ func (self *Actions) ContinueMerge() {
Confirm()
}
func (self *Actions) ContinueRebase() {
func (self *Common) ContinueRebase() {
self.ContinueMerge()
}
func (self *Actions) AcknowledgeConflicts() {
func (self *Common) AcknowledgeConflicts() {
self.t.ExpectPopup().Confirmation().
Title(Equals("Auto-merge failed")).
Content(Contains("Conflicts!")).
Confirm()
}
func (self *Actions) ContinueOnConflictsResolved() {
func (self *Common) ContinueOnConflictsResolved() {
self.t.ExpectPopup().Confirmation().
Title(Equals("continue")).
Content(Contains("all merge conflicts resolved. Continue?")).
Confirm()
}
func (self *Actions) ConfirmDiscardLines() {
func (self *Common) ConfirmDiscardLines() {
self.t.ExpectPopup().Confirmation().
Title(Equals("Unstage lines")).
Content(Contains("Are you sure you want to delete the selected lines")).
Confirm()
}
func (self *Actions) SelectPatchOption(matcher *Matcher) {
func (self *Common) SelectPatchOption(matcher *Matcher) {
self.t.GlobalPress(self.t.keys.Universal.CreatePatchOptionsMenu)
self.t.ExpectPopup().Menu().Title(Equals("Patch Options")).Select(matcher).Confirm()

View file

@ -31,7 +31,7 @@ func (self *MenuDriver) Cancel() {
}
func (self *MenuDriver) Select(option *Matcher) *MenuDriver {
self.getViewDriver().NavigateToListItem(option)
self.getViewDriver().NavigateToLine(option)
return self
}

View file

@ -3,9 +3,9 @@ package components
import "path/filepath"
// convenience struct for easily getting directories within our test directory.
// We have one test directory for each test, found in test/integration.
// We have one test directory for each test, found in test/results.
type Paths struct {
// e.g. test/integration/test_name
// e.g. test/results/test_name
root string
}

View file

@ -79,6 +79,6 @@ func (self *PromptDriver) ConfirmSuggestion(matcher *Matcher) {
self.t.press(self.t.keys.Universal.TogglePanel)
self.t.Views().Suggestions().
IsFocused().
NavigateToListItem(matcher).
NavigateToLine(matcher).
PressEnter()
}

View file

@ -11,14 +11,16 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
// this is the integration runner for the new and improved integration interface
const (
TEST_NAME_ENV_VAR = "TEST_NAME"
SANDBOX_ENV_VAR = "SANDBOX"
GIT_CONFIG_GLOBAL_ENV_VAR = "GIT_CONFIG_GLOBAL"
)
// This function lets you run tests either from within `go test` or from a regular binary.
// The reason for having two separate ways of testing is that `go test` isn't great at
// showing what's actually happening during the test, but it's still good at running
// tests in telling you about their results.
func RunTests(
tests []*IntegrationTest,
logf func(format string, formatArgs ...interface{}),
@ -34,7 +36,7 @@ func RunTests(
return err
}
testDir := filepath.Join(projectRootDir, "test", "integration_new")
testDir := filepath.Join(projectRootDir, "test", "results")
if err := buildLazygit(); err != nil {
return err

View file

@ -38,6 +38,15 @@ func (self *Shell) RunCommand(cmdStr string) *Shell {
return self
}
// Help files are located at test/files from the root the lazygit repo.
// E.g. You may want to create a pre-commit hook file there, then call this
// function to copy it into your test repo.
func (self *Shell) CopyHelpFile(source string, destination string) *Shell {
self.RunCommand(fmt.Sprintf("cp ../../../../../files/%s %s", source, destination))
return self
}
func (self *Shell) runCommandWithOutput(cmdStr string) (string, error) {
args := str.ToArgv(cmdStr)
cmd := secureexec.Command(args[0], args[1:]...)
@ -190,13 +199,27 @@ func (self *Shell) SetConfig(key string, value string) *Shell {
// creates a clone of the repo in a sibling directory and adds the clone
// as a remote, then fetches it.
func (self *Shell) CloneIntoRemote(name string) *Shell {
self.RunCommand(fmt.Sprintf("git clone --bare . ../%s", name))
self.Clone(name)
self.RunCommand(fmt.Sprintf("git remote add %s ../%s", name, name))
self.RunCommand(fmt.Sprintf("git fetch %s", name))
return self
}
func (self *Shell) CloneIntoSubmodule(submoduleName string) *Shell {
self.Clone("other_repo")
self.RunCommand(fmt.Sprintf("git submodule add ../other_repo %s", submoduleName))
return self
}
// clones repo into a sibling directory
func (self *Shell) Clone(repoName string) *Shell {
self.RunCommand(fmt.Sprintf("git clone --bare . ../%s", repoName))
return self
}
// e.g. branch: 'master', upstream: 'origin/master'
func (self *Shell) SetBranchUpstream(branch string, upstream string) *Shell {
self.RunCommand(fmt.Sprintf("git branch --set-upstream-to=%s %s", upstream, branch))

View file

@ -11,7 +11,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/utils"
)
// Test describes an integration tests that will be run against the lazygit gui.
// IntegrationTest describes an integration test that will be run against the lazygit gui.
// our unit tests will use this description to avoid a panic caused by attempting
// to get the test's name via it's file's path.

View file

@ -48,8 +48,8 @@ func (self *TestDriver) typeContent(content string) {
}
}
func (self *TestDriver) Actions() *Actions {
return &Actions{t: self}
func (self *TestDriver) Common() *Common {
return &Common{t: self}
}
// for when you want to allow lazygit to process something before continuing

View file

@ -11,6 +11,8 @@ import (
"github.com/stretchr/testify/assert"
)
// this file is for testing our test code (meta, I know)
type fakeGuiDriver struct {
failureMessage string
pressedKeys []string

View file

@ -63,6 +63,7 @@ func (self *ViewDriver) Title(expected *Matcher) *ViewDriver {
// asserts that the view has lines matching the given matchers. One matcher must be passed for each line.
// If you only care about the top n lines, use the TopLines method instead.
// If you only care about a subset of lines, use the ContainsLines method instead.
func (self *ViewDriver) Lines(matchers ...*Matcher) *ViewDriver {
self.validateMatchersPassed(matchers)
self.LineCount(len(matchers))
@ -81,6 +82,7 @@ func (self *ViewDriver) TopLines(matchers ...*Matcher) *ViewDriver {
return self.assertLines(0, matchers...)
}
// asserts that somewhere in the view there are consequetive lines matching the given matchers.
func (self *ViewDriver) ContainsLines(matchers ...*Matcher) *ViewDriver {
self.validateMatchersPassed(matchers)
self.validateEnoughLines(matchers)
@ -131,7 +133,7 @@ func (self *ViewDriver) ContainsLines(matchers ...*Matcher) *ViewDriver {
return self
}
// asserts on the lines that are selected in the view.
// asserts on the lines that are selected in the view. Don't use the `IsSelected` matcher with this because it's redundant.
func (self *ViewDriver) SelectedLines(matchers ...*Matcher) *ViewDriver {
self.validateMatchersPassed(matchers)
self.validateEnoughLines(matchers)
@ -373,7 +375,7 @@ func (self *ViewDriver) PressEscape() *ViewDriver {
// If this changes in future, we'll need to update this code to first attempt to find the item
// in the current page and failing that, jump to the top of the view and iterate through all of it,
// looking for the item.
func (self *ViewDriver) NavigateToListItem(matcher *Matcher) *ViewDriver {
func (self *ViewDriver) NavigateToLine(matcher *Matcher) *ViewDriver {
self.IsFocused()
view := self.getView()

View file

@ -13,16 +13,6 @@ type Views struct {
t *TestDriver
}
// not exporting this because I want the test to always be explicit about what
// view it's dealing with.
func (self *Views) current() *ViewDriver {
return &ViewDriver{
context: "current view",
getView: func() *gocui.View { return self.t.gui.CurrentContext().GetView() },
t: self.t,
}
}
func (self *Views) Main() *ViewDriver {
return &ViewDriver{
context: "main view",

View file

@ -32,14 +32,14 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits().
Focus().
SelectedLine(Contains("commit 10")).
NavigateToListItem(Contains("commit 09")).
NavigateToLine(Contains("commit 09")).
Tap(func() {
markCommitAsBad()
t.Views().Information().Content(Contains("bisecting"))
}).
SelectedLine(Contains("<-- bad")).
NavigateToListItem(Contains("commit 02")).
NavigateToLine(Contains("commit 02")).
Tap(markCommitAsGood).
// lazygit will land us in the commit between our good and bad commits.
SelectedLine(Contains("commit 05").Contains("<-- current")).

View file

@ -35,7 +35,7 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
Content(Contains("Are you sure you want to rebase 'first-change-branch' on top of 'second-change-branch'?")).
Confirm()
t.Actions().AcknowledgeConflicts()
t.Common().AcknowledgeConflicts()
t.Views().Files().
IsFocused().
@ -48,7 +48,7 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Information().Content(Contains("rebasing"))
t.Actions().ContinueOnConflictsResolved()
t.Common().ContinueOnConflictsResolved()
t.Views().Information().Content(DoesNotContain("rebasing"))

View file

@ -43,7 +43,7 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Information().Content(Contains("rebasing"))
t.Actions().AcknowledgeConflicts()
t.Common().AcknowledgeConflicts()
t.Views().Files().IsFocused().
SelectedLine(MatchesRegexp("UU.*file"))
@ -75,7 +75,7 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused().
PressPrimaryAction()
t.Actions().ContinueOnConflictsResolved()
t.Common().ContinueOnConflictsResolved()
t.Views().Information().Content(DoesNotContain("rebasing"))

View file

@ -52,7 +52,7 @@ var CherryPickConflicts = NewIntegrationTest(NewIntegrationTestArgs{
Content(Contains("Are you sure you want to cherry-pick the copied commits onto this branch?")).
Confirm()
t.Actions().AcknowledgeConflicts()
t.Common().AcknowledgeConflicts()
t.Views().Files().
IsFocused().
@ -65,7 +65,7 @@ var CherryPickConflicts = NewIntegrationTest(NewIntegrationTestArgs{
SelectNextItem().
PressPrimaryAction()
t.Actions().ContinueOnConflictsResolved()
t.Common().ContinueOnConflictsResolved()
t.Views().Files().IsEmpty()

View file

@ -25,7 +25,7 @@ var ResolveExternally = NewIntegrationTest(NewIntegrationTestArgs{
}).
Press(keys.Universal.Refresh)
t.Actions().ContinueOnConflictsResolved()
t.Common().ContinueOnConflictsResolved()
t.Views().Files().
IsEmpty()

View file

@ -49,6 +49,6 @@ var ResolveMultipleFiles = NewIntegrationTest(NewIntegrationTestArgs{
).
PressPrimaryAction()
t.Actions().ContinueOnConflictsResolved()
t.Common().ContinueOnConflictsResolved()
},
})

View file

@ -99,7 +99,7 @@ var DiscardChanges = NewIntegrationTest(NewIntegrationTestArgs{
{status: "DU", label: "deleted-us.txt", menuTitle: "deleted-us.txt"},
})
t.Actions().ContinueOnConflictsResolved()
t.Common().ContinueOnConflictsResolved()
discardOneByOne([]statusFile{
{status: "MD", label: "change-delete.txt", menuTitle: "change-delete.txt"},

View file

@ -37,40 +37,3 @@ var SelectFile = NewIntegrationTest(NewIntegrationTestArgs{
postFilterTest(t)
},
})
func commonSetup(shell *Shell) {
shell.CreateFileAndAdd("filterFile", "original filterFile content")
shell.CreateFileAndAdd("otherFile", "original otherFile content")
shell.Commit("both files")
shell.UpdateFileAndAdd("otherFile", "new otherFile content")
shell.Commit("only otherFile")
shell.UpdateFileAndAdd("filterFile", "new filterFile content")
shell.Commit("only filterFile")
}
func postFilterTest(t *TestDriver) {
t.Views().Information().Content(Contains("filtering by 'filterFile'"))
t.Views().Commits().
IsFocused().
Lines(
Contains(`only filterFile`).IsSelected(),
Contains(`both files`),
).
SelectNextItem().
PressEnter()
// we only show the filtered file's changes in the main view
t.Views().Main().
Content(Contains("filterFile").DoesNotContain("otherFile"))
// when you click into the commit itself, you see all files from that commit
t.Views().CommitFiles().
IsFocused().
Lines(
Contains(`filterFile`),
Contains(`otherFile`),
)
}

View file

@ -0,0 +1,42 @@
package filter_by_path
import (
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
func commonSetup(shell *Shell) {
shell.CreateFileAndAdd("filterFile", "original filterFile content")
shell.CreateFileAndAdd("otherFile", "original otherFile content")
shell.Commit("both files")
shell.UpdateFileAndAdd("otherFile", "new otherFile content")
shell.Commit("only otherFile")
shell.UpdateFileAndAdd("filterFile", "new filterFile content")
shell.Commit("only filterFile")
}
func postFilterTest(t *TestDriver) {
t.Views().Information().Content(Contains("filtering by 'filterFile'"))
t.Views().Commits().
IsFocused().
Lines(
Contains(`only filterFile`).IsSelected(),
Contains(`both files`),
).
SelectNextItem().
PressEnter()
// we only show the filtered file's changes in the main view
t.Views().Main().
Content(Contains("filterFile").DoesNotContain("otherFile"))
// when you click into the commit itself, you see all files from that commit
t.Views().CommitFiles().
IsFocused().
Lines(
Contains(`filterFile`),
Contains(`otherFile`),
)
}

View file

@ -22,7 +22,7 @@ var AmendFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToListItem(Contains("commit 01")).
NavigateToLine(Contains("commit 01")).
Press(keys.Commits.AmendToCommit).
Tap(func() {
t.ExpectPopup().Confirmation().

View file

@ -21,14 +21,14 @@ var EditFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToListItem(Contains("commit 01")).
NavigateToLine(Contains("commit 01")).
Press(keys.Universal.Edit).
Lines(
Contains("commit 02"),
MatchesRegexp("YOU ARE HERE.*commit 01").IsSelected(),
).
Tap(func() {
t.Actions().ContinueRebase()
t.Common().ContinueRebase()
}).
Lines(
Contains("commit 02"),

View file

@ -21,7 +21,7 @@ var FixupFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToListItem(Contains("commit 01")).
NavigateToLine(Contains("commit 01")).
Press(keys.Commits.MarkCommitAsFixup).
Tap(func() {
t.ExpectPopup().Alert().

View file

@ -22,7 +22,7 @@ var FixupSecondCommit = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToListItem(Contains("commit 02")).
NavigateToLine(Contains("commit 02")).
Press(keys.Commits.MarkCommitAsFixup).
Tap(func() {
t.ExpectPopup().Confirmation().

View file

@ -22,7 +22,7 @@ var MoveInRebase = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToListItem(Contains("commit 01")).
NavigateToLine(Contains("commit 01")).
Press(keys.Universal.Edit).
Lines(
Contains("commit 04"),
@ -84,7 +84,7 @@ var MoveInRebase = NewIntegrationTest(NewIntegrationTestArgs{
Contains("YOU ARE HERE").Contains("commit 01"),
).
Tap(func() {
t.Actions().ContinueRebase()
t.Common().ContinueRebase()
}).
Lines(
Contains("commit 04"),

View file

@ -31,7 +31,7 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
Contains("first commit to edit"),
Contains("initial commit"),
).
NavigateToListItem(Contains("first commit to edit")).
NavigateToLine(Contains("first commit to edit")).
Press(keys.Universal.Edit).
Lines(
MatchesRegexp("pick.*commit to fixup"),
@ -82,7 +82,7 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
Contains("initial commit"),
).
Tap(func() {
t.Actions().ContinueRebase()
t.Common().ContinueRebase()
}).
Lines(
MatchesRegexp("fixup.*commit to fixup").IsSelected(),
@ -92,7 +92,7 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
Contains("initial commit"),
).
Tap(func() {
t.Actions().ContinueRebase()
t.Common().ContinueRebase()
}).
Lines(
Contains("second commit to edit").IsSelected(),

View file

@ -24,7 +24,7 @@ var RewordFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToListItem(Contains("commit 01")).
NavigateToLine(Contains("commit 01")).
Press(keys.Commits.RenameCommit).
Tap(func() {
t.ExpectPopup().Prompt().

View file

@ -0,0 +1,68 @@
package interactive_rebase
import (
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
func handleConflictsFromSwap(t *TestDriver) {
t.Common().AcknowledgeConflicts()
t.Views().Files().
IsFocused().
Lines(
Contains("UU myfile"),
).
PressEnter()
t.Views().MergeConflicts().
IsFocused().
TopLines(
Contains("<<<<<<< HEAD"),
Contains("one"),
Contains("======="),
Contains("three"),
Contains(">>>>>>>"),
).
SelectNextItem().
PressPrimaryAction() // pick "three"
t.Common().ContinueOnConflictsResolved()
t.Common().AcknowledgeConflicts()
t.Views().Files().
IsFocused().
Lines(
Contains("UU myfile"),
).
PressEnter()
t.Views().MergeConflicts().
IsFocused().
TopLines(
Contains("<<<<<<< HEAD"),
Contains("three"),
Contains("======="),
Contains("two"),
Contains(">>>>>>>"),
).
SelectNextItem().
PressPrimaryAction() // pick "two"
t.Common().ContinueOnConflictsResolved()
t.Views().Commits().
Focus().
Lines(
Contains("commit two").IsSelected(),
Contains("commit three"),
Contains("commit one"),
).
Tap(func() {
t.Views().Main().Content(Contains("-three").Contains("+two"))
}).
SelectNextItem().
Tap(func() {
t.Views().Main().Content(Contains("-one").Contains("+three"))
})
}

View file

@ -21,7 +21,7 @@ var SquashDownFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToListItem(Contains("commit 01")).
NavigateToLine(Contains("commit 01")).
Press(keys.Commits.SquashDown).
Tap(func() {
t.ExpectPopup().Alert().

View file

@ -22,7 +22,7 @@ var SquashDownSecondCommit = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToListItem(Contains("commit 02")).
NavigateToLine(Contains("commit 02")).
Press(keys.Commits.SquashDown).
Tap(func() {
t.ExpectPopup().Confirmation().

View file

@ -22,7 +22,7 @@ var SquashFixupsAboveFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit 02"),
Contains("commit 01"),
).
NavigateToListItem(Contains("commit 01")).
NavigateToLine(Contains("commit 01")).
Press(keys.Commits.CreateFixupCommit).
Tap(func() {
t.ExpectPopup().Confirmation().
@ -30,7 +30,7 @@ var SquashFixupsAboveFirstCommit = NewIntegrationTest(NewIntegrationTestArgs{
Content(Contains("Are you sure you want to create a fixup! commit for commit")).
Confirm()
}).
NavigateToListItem(Contains("commit 01")).
NavigateToLine(Contains("commit 01")).
Press(keys.Commits.SquashAboveCommits).
Tap(func() {
t.ExpectPopup().Confirmation().

View file

@ -26,7 +26,7 @@ var SwapInRebaseWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
Contains("commit two"),
Contains("commit one"),
).
NavigateToListItem(Contains("commit one")).
NavigateToLine(Contains("commit one")).
Press(keys.Universal.Edit).
Lines(
Contains("commit three"),
@ -41,72 +41,9 @@ var SwapInRebaseWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
Contains("YOU ARE HERE").Contains("commit one"),
).
Tap(func() {
t.Actions().ContinueRebase()
t.Common().ContinueRebase()
})
handleConflictsFromSwap(t)
},
})
func handleConflictsFromSwap(t *TestDriver) {
t.Actions().AcknowledgeConflicts()
t.Views().Files().
IsFocused().
Lines(
Contains("UU myfile"),
).
PressEnter()
t.Views().MergeConflicts().
IsFocused().
TopLines(
Contains("<<<<<<< HEAD"),
Contains("one"),
Contains("======="),
Contains("three"),
Contains(">>>>>>>"),
).
SelectNextItem().
PressPrimaryAction() // pick "three"
t.Actions().ContinueOnConflictsResolved()
t.Actions().AcknowledgeConflicts()
t.Views().Files().
IsFocused().
Lines(
Contains("UU myfile"),
).
PressEnter()
t.Views().MergeConflicts().
IsFocused().
TopLines(
Contains("<<<<<<< HEAD"),
Contains("three"),
Contains("======="),
Contains("two"),
Contains(">>>>>>>"),
).
SelectNextItem().
PressPrimaryAction() // pick "two"
t.Actions().ContinueOnConflictsResolved()
t.Views().Commits().
Focus().
Lines(
Contains("commit two").IsSelected(),
Contains("commit three"),
Contains("commit one"),
).
Tap(func() {
t.Views().Main().Content(Contains("-three").Contains("+two"))
}).
SelectNextItem().
Tap(func() {
t.Views().Main().Content(Contains("-one").Contains("+three"))
})
}

View file

@ -50,7 +50,7 @@ var Apply = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().PatchBuildingSecondary().Content(Contains("second line"))
t.Actions().SelectPatchOption(MatchesRegexp(`apply patch$`))
t.Common().SelectPatchOption(MatchesRegexp(`apply patch$`))
t.Views().Files().
Focus().

View file

@ -35,7 +35,7 @@ var ApplyInReverse = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().PatchBuildingSecondary().Content(Contains("+file1 content"))
t.Actions().SelectPatchOption(Contains("apply patch in reverse"))
t.Common().SelectPatchOption(Contains("apply patch in reverse"))
t.Views().Files().
Focus().

View file

@ -40,7 +40,7 @@ var CopyPatchToClipboard = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Information().Content(Contains("building patch"))
t.Actions().SelectPatchOption(Contains("copy patch to clipboard"))
t.Common().SelectPatchOption(Contains("copy patch to clipboard"))
t.ExpectToast(Contains("Patch copied to clipboard"))

View file

@ -35,7 +35,7 @@ var MoveToIndex = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().PatchBuildingSecondary().Content(Contains("+file1 content"))
t.Actions().SelectPatchOption(Contains("move patch out into index"))
t.Common().SelectPatchOption(Contains("move patch out into index"))
t.Views().Files().
Lines(

View file

@ -28,7 +28,7 @@ var MoveToIndexPartial = NewIntegrationTest(NewIntegrationTestArgs{
Contains("second commit"),
Contains("first commit"),
).
NavigateToListItem(Contains("second commit")).
NavigateToLine(Contains("second commit")).
PressEnter()
t.Views().CommitFiles().
@ -61,7 +61,7 @@ var MoveToIndexPartial = NewIntegrationTest(NewIntegrationTestArgs{
Contains(` third line`),
)
t.Actions().SelectPatchOption(Contains("move patch out into index"))
t.Common().SelectPatchOption(Contains("move patch out into index"))
t.Views().Files().
Lines(

View file

@ -40,9 +40,9 @@ var MoveToIndexWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Information().Content(Contains("building patch"))
t.Actions().SelectPatchOption(Contains("move patch out into index"))
t.Common().SelectPatchOption(Contains("move patch out into index"))
t.Actions().AcknowledgeConflicts()
t.Common().AcknowledgeConflicts()
t.Views().Files().
IsFocused().
@ -62,7 +62,7 @@ var MoveToIndexWithConflict = NewIntegrationTest(NewIntegrationTestArgs{
).
PressPrimaryAction()
t.Actions().ContinueOnConflictsResolved()
t.Common().ContinueOnConflictsResolved()
t.ExpectPopup().Alert().
Title(Equals("Error")).

View file

@ -40,7 +40,7 @@ var MoveToNewCommit = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Information().Content(Contains("building patch"))
t.Actions().SelectPatchOption(Contains("move patch into new commit"))
t.Common().SelectPatchOption(Contains("move patch into new commit"))
t.Views().CommitFiles().
IsFocused().

View file

@ -35,7 +35,7 @@ var RemoveFromCommit = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().PatchBuildingSecondary().Content(Contains("+file1 content"))
t.Actions().SelectPatchOption(Contains("remove patch from original commit"))
t.Common().SelectPatchOption(Contains("remove patch from original commit"))
t.Views().Files().IsEmpty()

View file

@ -43,7 +43,7 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Secondary().Content(Contains("direct file content"))
}).
NavigateToListItem(Contains("hunk-file")).
NavigateToLine(Contains("hunk-file")).
PressEnter()
t.Views().PatchBuilding().
@ -92,7 +92,7 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().CommitFiles().
IsFocused().
NavigateToListItem(Contains("line-file")).
NavigateToLine(Contains("line-file")).
PressEnter()
t.Views().PatchBuilding().
@ -106,11 +106,11 @@ var SpecificSelection = NewIntegrationTest(NewIntegrationTestArgs{
Contains("+2a"),
).
PressPrimaryAction().
NavigateToListItem(Contains("+2c")).
NavigateToLine(Contains("+2c")).
Press(keys.Main.ToggleDragSelect).
NavigateToListItem(Contains("+2e")).
NavigateToLine(Contains("+2e")).
PressPrimaryAction().
NavigateToListItem(Contains("+2g")).
NavigateToLine(Contains("+2g")).
PressPrimaryAction().
Tap(func() {
t.Views().Information().Content(Contains("building patch"))

View file

@ -41,7 +41,7 @@ var StartNewPatch = NewIntegrationTest(NewIntegrationTestArgs{
t.Views().Commits().
IsFocused().
NavigateToListItem(Contains("first commit")).
NavigateToLine(Contains("first commit")).
PressEnter()
t.Views().CommitFiles().

View file

@ -33,13 +33,13 @@ var DiscardAllChanges = NewIntegrationTest(NewIntegrationTestArgs{
// discard the line
Press(keys.Universal.Remove).
Tap(func() {
t.Actions().ConfirmDiscardLines()
t.Common().ConfirmDiscardLines()
}).
SelectedLines(Contains("+four")).
// discard the other line
Press(keys.Universal.Remove).
Tap(func() {
t.Actions().ConfirmDiscardLines()
t.Common().ConfirmDiscardLines()
// because there are no more changes in file1 we switch to file2
t.Views().Files().

View file

@ -139,7 +139,7 @@ var StageHunks = NewIntegrationTest(NewIntegrationTestArgs{
).
Press(keys.Universal.Remove).
Tap(func() {
t.Actions().ConfirmDiscardLines()
t.Common().ConfirmDiscardLines()
}).
Content(DoesNotContain("-3a").DoesNotContain("+3b")).
SelectedLines(

View file

@ -30,7 +30,7 @@ var StageRanges = NewIntegrationTest(NewIntegrationTestArgs{
Contains("+three"),
).
Press(keys.Main.ToggleDragSelect).
NavigateToListItem(Contains("+five")).
NavigateToLine(Contains("+five")).
SelectedLines(
Contains("+three"),
Contains("+four"),
@ -61,7 +61,7 @@ var StageRanges = NewIntegrationTest(NewIntegrationTestArgs{
Contains("+three"),
).
Press(keys.Main.ToggleDragSelect).
NavigateToListItem(Contains("+five")).
NavigateToLine(Contains("+five")).
SelectedLines(
Contains("+three"),
Contains("+four"),
@ -96,7 +96,7 @@ var StageRanges = NewIntegrationTest(NewIntegrationTestArgs{
).
Press(keys.Universal.Remove).
Tap(func() {
t.Actions().ConfirmDiscardLines()
t.Common().ConfirmDiscardLines()
}).
ContainsLines(
Contains("+three"),

View file

@ -12,7 +12,7 @@ var Add = NewIntegrationTest(NewIntegrationTestArgs{
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("first commit")
shell.RunCommand("git clone --bare . ../other_repo")
shell.Clone("other_repo")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Submodules().Focus().

View file

@ -20,8 +20,7 @@ var Enter = NewIntegrationTest(NewIntegrationTestArgs{
},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("first commit")
shell.RunCommand("git clone --bare . ../other_repo")
shell.RunCommand("git submodule add ../other_repo my_submodule")
shell.CloneIntoSubmodule("my_submodule")
shell.GitAddAll()
shell.Commit("add submodule")
},

View file

@ -12,8 +12,7 @@ var Remove = NewIntegrationTest(NewIntegrationTestArgs{
SetupConfig: func(config *config.AppConfig) {},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("first commit")
shell.RunCommand("git clone --bare . ../other_repo")
shell.RunCommand("git submodule add ../other_repo my_submodule")
shell.CloneIntoSubmodule("my_submodule")
shell.GitAddAll()
shell.Commit("add submodule")
},

View file

@ -20,8 +20,7 @@ var Reset = NewIntegrationTest(NewIntegrationTestArgs{
},
SetupRepo: func(shell *Shell) {
shell.EmptyCommit("first commit")
shell.RunCommand("git clone --bare . ../other_repo")
shell.RunCommand("git submodule add ../other_repo my_submodule")
shell.CloneIntoSubmodule("my_submodule")
shell.GitAddAll()
shell.Commit("add submodule")
},

View file

@ -5,25 +5,6 @@ import (
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
func createTwoBranchesReadyToForcePush(shell *Shell) {
shell.EmptyCommit("one")
shell.EmptyCommit("two")
shell.NewBranch("other_branch")
shell.CloneIntoRemote("origin")
shell.SetBranchUpstream("master", "origin/master")
shell.SetBranchUpstream("other_branch", "origin/other_branch")
// remove the 'two' commit so that we have something to pull from the remote
shell.HardReset("HEAD^")
shell.Checkout("master")
// doing the same for master
shell.HardReset("HEAD^")
}
var ForcePushMultipleUpstream = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Force push to only the upstream branch of the current branch because the user has push.default upstream",
ExtraCmdArgs: "",

View file

@ -40,7 +40,7 @@ var PullMergeConflict = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused().
Press(keys.Universal.Pull)
t.Actions().AcknowledgeConflicts()
t.Common().AcknowledgeConflicts()
t.Views().Files().
IsFocused().
@ -60,7 +60,7 @@ var PullMergeConflict = NewIntegrationTest(NewIntegrationTestArgs{
).
PressPrimaryAction() // choose 'content4'
t.Actions().ContinueOnConflictsResolved()
t.Common().ContinueOnConflictsResolved()
t.Views().Status().Content(Contains("↑2 repo → master"))

View file

@ -40,7 +40,7 @@ var PullRebaseConflict = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused().
Press(keys.Universal.Pull)
t.Actions().AcknowledgeConflicts()
t.Common().AcknowledgeConflicts()
t.Views().Files().
IsFocused().
@ -61,7 +61,7 @@ var PullRebaseConflict = NewIntegrationTest(NewIntegrationTestArgs{
SelectNextItem().
PressPrimaryAction() // choose 'content4'
t.Actions().ContinueOnConflictsResolved()
t.Common().ContinueOnConflictsResolved()
t.Views().Status().Content(Contains("↑1 repo → master"))

View file

@ -42,7 +42,7 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{
IsFocused().
Press(keys.Universal.Pull)
t.Actions().AcknowledgeConflicts()
t.Common().AcknowledgeConflicts()
t.Views().Commits().
Lines(
@ -71,7 +71,7 @@ var PullRebaseInteractiveConflict = NewIntegrationTest(NewIntegrationTestArgs{
SelectNextItem().
PressPrimaryAction() // choose 'content4'
t.Actions().ContinueOnConflictsResolved()
t.Common().ContinueOnConflictsResolved()
t.Views().Status().Content(Contains("↑2 repo → master"))

View file

@ -42,7 +42,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg
IsFocused().
Press(keys.Universal.Pull)
t.Actions().AcknowledgeConflicts()
t.Common().AcknowledgeConflicts()
t.Views().Commits().
Focus().
@ -79,7 +79,7 @@ var PullRebaseInteractiveConflictDrop = NewIntegrationTest(NewIntegrationTestArg
SelectNextItem().
PressPrimaryAction() // choose 'content4'
t.Actions().ContinueOnConflictsResolved()
t.Common().ContinueOnConflictsResolved()
t.Views().Status().Content(Contains("↑1 repo → master"))

View file

@ -30,28 +30,3 @@ var Push = NewIntegrationTest(NewIntegrationTestArgs{
assertSuccessfullyPushed(t)
},
})
func assertSuccessfullyPushed(t *TestDriver) {
t.Views().Status().Content(Contains("✓ repo → master"))
t.Views().Remotes().
Focus().
Lines(
Contains("origin"),
).
PressEnter()
t.Views().RemoteBranches().
IsFocused().
Lines(
Contains("master"),
).
PressEnter()
t.Views().SubCommits().
IsFocused().
Lines(
Contains("two"),
Contains("one"),
)
}

View file

@ -23,8 +23,7 @@ var PushWithCredentialPrompt = NewIntegrationTest(NewIntegrationTestArgs{
// actually getting a password prompt is tricky: it requires SSH'ing into localhost under a newly created, restricted, user.
// This is not easy to do in a cross-platform way, nor is it easy to do in a docker container.
// If you can think of a way to do it, please let me know!
shell.RunCommand("cp ../../../../../hooks/pre-push .git/hooks/pre-push")
shell.RunCommand("chmod +x .git/hooks/pre-push")
shell.CopyHelpFile("pre-push", ".git/hooks/pre-push")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Status().Content(Contains("↑1 repo → master"))

View file

@ -0,0 +1,49 @@
package sync
import (
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
func createTwoBranchesReadyToForcePush(shell *Shell) {
shell.EmptyCommit("one")
shell.EmptyCommit("two")
shell.NewBranch("other_branch")
shell.CloneIntoRemote("origin")
shell.SetBranchUpstream("master", "origin/master")
shell.SetBranchUpstream("other_branch", "origin/other_branch")
// remove the 'two' commit so that we have something to pull from the remote
shell.HardReset("HEAD^")
shell.Checkout("master")
// doing the same for master
shell.HardReset("HEAD^")
}
func assertSuccessfullyPushed(t *TestDriver) {
t.Views().Status().Content(Contains("✓ repo → master"))
t.Views().Remotes().
Focus().
Lines(
Contains("origin"),
).
PressEnter()
t.Views().RemoteBranches().
IsFocused().
Lines(
Contains("master"),
).
PressEnter()
t.Views().SubCommits().
IsFocused().
Lines(
Contains("two"),
Contains("one"),
)
}

View file

@ -1,7 +1,7 @@
//go:build ignore
// This file is invoked with `go generate ./...` and it generates the tests_gen.go file
// The tests_gen.go file is a list of all the integration tests.
// This file is invoked with `go generate ./...` and it generates the test_list.go file
// The test_list.go file is a list of all the integration tests.
// It's annoying to have to manually add an entry in that file for each test you
// create, so this generator is here to make the process easier.
@ -26,7 +26,7 @@ func main() {
if err != nil {
panic(err)
}
if err := ioutil.WriteFile("tests_gen.go", formattedCode, 0o644); err != nil {
if err := ioutil.WriteFile("test_list.go", formattedCode, 0o644); err != nil {
panic(err)
}
}

View file

@ -1,4 +1,4 @@
//go:generate go run tests_generator.go
//go:generate go run test_list_generator.go
package tests
@ -30,7 +30,7 @@ func GetTests() []*components.IntegrationTest {
if err := filepath.Walk(filepath.Join(utils.GetLazyRootDirectory(), "pkg/integration/tests"), func(path string, info os.FileInfo, err error) error {
if !info.IsDir() && strings.HasSuffix(path, ".go") {
// ignoring non-test files
if filepath.Base(path) == "tests.go" || filepath.Base(path) == "tests_gen.go" || filepath.Base(path) == "tests_generator.go" {
if filepath.Base(path) == "tests.go" || filepath.Base(path) == "test_list.go" || filepath.Base(path) == "test_list_generator.go" {
return nil
}
@ -39,6 +39,11 @@ func GetTests() []*components.IntegrationTest {
return nil
}
// any file named shared.go will also be ignored, because those files are only used for shared helper functions
if filepath.Base(path) == "shared.go" {
return nil
}
nameFromPath := components.TestNameFromFilePath(path)
if !testNamesSet.Includes(nameFromPath) {
missingTestNames = append(missingTestNames, nameFromPath)
@ -51,13 +56,13 @@ func GetTests() []*components.IntegrationTest {
}
if len(missingTestNames) > 0 {
panic(fmt.Sprintf("The following tests are missing from the list of tests: %s. You need to add them to `pkg/integration/tests/tests_gen.go`. Use `go generate ./...` to regenerate the tests list.", strings.Join(missingTestNames, ", ")))
panic(fmt.Sprintf("The following tests are missing from the list of tests: %s. You need to add them to `pkg/integration/tests/test_list.go`. Use `go generate ./...` to regenerate the tests list.", strings.Join(missingTestNames, ", ")))
}
if testCount > len(tests) {
panic("you have not added all of the tests to the tests list in `pkg/integration/tests/tests_gen.go`. Use `go generate ./...` to regenerate the tests list.")
panic("you have not added all of the tests to the tests list in `pkg/integration/tests/test_list.go`. Use `go generate ./...` to regenerate the tests list.")
} else if testCount < len(tests) {
panic("There are more tests in `pkg/integration/tests/tests_gen.go` than there are test files in the tests directory. Ensure that you only have one test per file and you haven't included the same test twice in the tests list. Use `go generate ./...` to regenerate the tests list.")
panic("There are more tests in `pkg/integration/tests/test_list.go` than there are test files in the tests directory. Ensure that you only have one test per file and you haven't included the same test twice in the tests list. Use `go generate ./...` to regenerate the tests list.")
}
return tests

2
test/README.md Normal file
View file

@ -0,0 +1,2 @@
This directory contains some files used by out integration tests. The tests themselves live in [/pkg/integration/](/pkg/integration/). See [here](/pkg/integration/README.md) for more info

0
test/hooks/pre-push → test/files/pre-push Normal file → Executable file
View file

View file

@ -1,3 +1,5 @@
# This is the global git config we use for all our integration tests
[user]
name = CI
email = CI@example.com