Support project view --web with TTY (#8773)

This commit is contained in:
Harvey Sanders 2024-03-01 11:45:29 -05:00 committed by GitHub
parent 590208f5d6
commit 9dd102ffd0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 261 additions and 72 deletions

View file

@ -29,16 +29,31 @@ func NewClient(httpClient *http.Client, hostname string, ios *iostreams.IOStream
}
}
func NewTestClient() *Client {
// TestClientOpt is a test option for the test client.
type TestClientOpt func(*Client)
// WithPrompter is a test option to set the prompter for the test client.
func WithPrompter(p iprompter) TestClientOpt {
return func(c *Client) {
c.prompter = p
}
}
func NewTestClient(opts ...TestClientOpt) *Client {
apiClient := &hostScopedClient{
hostname: "github.com",
Client: api.NewClientFromHTTP(http.DefaultClient),
}
return &Client{
c := &Client{
apiClient: apiClient,
spinner: false,
prompter: nil,
}
for _, o := range opts {
o(c)
}
return c
}
type iprompter interface {

View file

@ -82,18 +82,6 @@ func NewCmdView(f *cmdutil.Factory, runF func(config viewConfig) error) *cobra.C
}
func runView(config viewConfig) error {
if config.opts.web {
url, err := buildURL(config)
if err != nil {
return err
}
if err := config.URLOpener(url); err != nil {
return err
}
return nil
}
canPrompt := config.io.CanPrompt()
owner, err := config.client.NewOwner(canPrompt, config.opts.owner)
if err != nil {
@ -105,6 +93,10 @@ func runView(config viewConfig) error {
return err
}
if config.opts.web {
return config.URLOpener(project.URL)
}
if config.opts.exporter != nil {
return config.opts.exporter.Write(config.io, *project)
}
@ -112,31 +104,6 @@ func runView(config viewConfig) error {
return printResults(config, project)
}
// TODO: support non-github.com hostnames
func buildURL(config viewConfig) (string, error) {
var url string
if config.opts.owner == "@me" {
owner, err := config.client.ViewerLoginName()
if err != nil {
return "", err
}
url = fmt.Sprintf("https://github.com/users/%s/projects/%d", owner, config.opts.number)
} else {
_, ownerType, err := config.client.OwnerIDAndType(config.opts.owner)
if err != nil {
return "", err
}
if ownerType == queries.UserOwner {
url = fmt.Sprintf("https://github.com/users/%s/projects/%d", config.opts.owner, config.opts.number)
} else {
url = fmt.Sprintf("https://github.com/orgs/%s/projects/%d", config.opts.owner, config.opts.number)
}
}
return url, nil
}
func printResults(config viewConfig, project *queries.Project) error {
var sb strings.Builder
sb.WriteString("# Title\n")

View file

@ -4,6 +4,7 @@ import (
"bytes"
"testing"
"github.com/cli/cli/v2/internal/prompter"
"github.com/cli/cli/v2/pkg/cmd/project/shared/queries"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
@ -90,35 +91,6 @@ func TestNewCmdview(t *testing.T) {
}
}
func TestBuildURLViewer(t *testing.T) {
defer gock.Off()
gock.New("https://api.github.com").
Post("/graphql").
Reply(200).
JSON(`
{"data":
{"viewer":
{
"login":"theviewer"
}
}
}
`)
client := queries.NewTestClient()
url, err := buildURL(viewConfig{
opts: viewOpts{
number: 1,
owner: "@me",
},
client: client,
})
assert.NoError(t, err)
assert.Equal(t, "https://github.com/users/theviewer/projects/1", url)
}
func TestRunView_User(t *testing.T) {
defer gock.Off()
@ -223,6 +195,7 @@ func TestRunView_Viewer(t *testing.T) {
"items": {
"totalCount": 10
},
"url":"https://github.com/orgs/github/projects/8",
"readme": null,
"fields": {
"nodes": [
@ -294,6 +267,7 @@ func TestRunView_Org(t *testing.T) {
"items": {
"totalCount": 10
},
"url":"https://github.com/orgs/github/projects/8",
"readme": null,
"fields": {
"nodes": [
@ -361,10 +335,11 @@ func TestRunViewWeb_User(t *testing.T) {
{
"login":"monalisa",
"projectV2": {
"number": 1,
"number": 8,
"items": {
"totalCount": 10
},
"url":"https://github.com/users/monalisa/projects/8",
"readme": null,
"fields": {
"nodes": [
@ -381,6 +356,7 @@ func TestRunViewWeb_User(t *testing.T) {
client := queries.NewTestClient()
buf := bytes.Buffer{}
ios, _, _, _ := iostreams.Test()
config := viewConfig{
opts: viewOpts{
owner: "monalisa",
@ -392,6 +368,7 @@ func TestRunViewWeb_User(t *testing.T) {
return nil
},
client: client,
io: ios,
}
err := runView(config)
@ -436,10 +413,11 @@ func TestRunViewWeb_Org(t *testing.T) {
{
"login":"github",
"projectV2": {
"number": 1,
"number": 8,
"items": {
"totalCount": 10
},
"url": "https://github.com/orgs/github/projects/8",
"readme": null,
"fields": {
"nodes": [
@ -456,6 +434,7 @@ func TestRunViewWeb_Org(t *testing.T) {
client := queries.NewTestClient()
buf := bytes.Buffer{}
ios, _, _, _ := iostreams.Test()
config := viewConfig{
opts: viewOpts{
owner: "github",
@ -467,6 +446,7 @@ func TestRunViewWeb_Org(t *testing.T) {
return nil
},
client: client,
io: ios,
}
err := runView(config)
@ -496,18 +476,24 @@ func TestRunViewWeb_Me(t *testing.T) {
gock.New("https://api.github.com").
Post("/graphql").
MatchType("json").
JSON(map[string]interface{}{
"query": "query ViewerProject.*",
"variables": map[string]interface{}{"afterFields": nil, "afterItems": nil, "firstFields": 100, "firstItems": 0, "number": 8},
}).
Reply(200).
JSON(`
{"data":
{"user":
{"viewer":
{
"login":"github",
"projectV2": {
"number": 1,
"number": 8,
"items": {
"totalCount": 10
},
"readme": null,
"url": "https://github.com/users/theviewer/projects/8",
"fields": {
"nodes": [
{
@ -523,6 +509,7 @@ func TestRunViewWeb_Me(t *testing.T) {
client := queries.NewTestClient()
buf := bytes.Buffer{}
ios, _, _, _ := iostreams.Test()
config := viewConfig{
opts: viewOpts{
owner: "@me",
@ -534,9 +521,229 @@ func TestRunViewWeb_Me(t *testing.T) {
return nil
},
client: client,
io: ios,
}
err := runView(config)
assert.NoError(t, err)
assert.Equal(t, "https://github.com/users/theviewer/projects/8", buf.String())
}
func TestRunViewWeb_TTY(t *testing.T) {
tests := []struct {
name string
setup func(*viewOpts, *testing.T) func()
promptStubs func(*prompter.PrompterMock)
gqlStubs func(*testing.T)
expectedBrowse string
tty bool
}{
{
name: "Org project --web with tty",
tty: true,
setup: func(vo *viewOpts, t *testing.T) func() {
return func() {
vo.web = true
}
},
gqlStubs: func(t *testing.T) {
gock.New("https://api.github.com").
Post("/graphql").
MatchType("json").
JSON(map[string]interface{}{
"query": "query ViewerLoginAndOrgs.*",
"variables": map[string]interface{}{
"after": nil,
},
}).
Reply(200).
JSON(map[string]interface{}{
"data": map[string]interface{}{
"viewer": map[string]interface{}{
"id": "monalisa-ID",
"login": "monalisa",
"organizations": map[string]interface{}{
"nodes": []interface{}{
map[string]interface{}{
"login": "github",
"viewerCanCreateProjects": true,
},
},
},
},
},
})
gock.New("https://api.github.com").
Post("/graphql").
MatchType("json").
JSON(map[string]interface{}{
"query": "query OrgProjects.*",
"variables": map[string]interface{}{
"after": nil,
"afterFields": nil,
"afterItems": nil,
"first": 30,
"firstFields": 100,
"firstItems": 0,
"login": "github",
},
}).
Reply(200).
JSON(map[string]interface{}{
"data": map[string]interface{}{
"organization": map[string]interface{}{
"login": "github",
"projectsV2": map[string]interface{}{
"nodes": []interface{}{
map[string]interface{}{
"id": "a-project-ID",
"title": "Get it done!",
"url": "https://github.com/orgs/github/projects/1",
"number": 1,
},
},
},
},
},
})
},
promptStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
switch prompt {
case "Which owner would you like to use?":
return prompter.IndexFor(opts, "github")
case "Which project would you like to use?":
return prompter.IndexFor(opts, "Get it done! (#1)")
default:
return -1, prompter.NoSuchPromptErr(prompt)
}
}
},
expectedBrowse: "https://github.com/orgs/github/projects/1",
},
{
name: "User project --web with tty",
tty: true,
setup: func(vo *viewOpts, t *testing.T) func() {
return func() {
vo.web = true
}
},
gqlStubs: func(t *testing.T) {
gock.New("https://api.github.com").
Post("/graphql").
MatchType("json").
JSON(map[string]interface{}{
"query": "query ViewerLoginAndOrgs.*",
"variables": map[string]interface{}{
"after": nil,
},
}).
Reply(200).
JSON(map[string]interface{}{
"data": map[string]interface{}{
"viewer": map[string]interface{}{
"id": "monalisa-ID",
"login": "monalisa",
"organizations": map[string]interface{}{
"nodes": []interface{}{
map[string]interface{}{
"login": "github",
"viewerCanCreateProjects": true,
},
},
},
},
},
})
gock.New("https://api.github.com").
Post("/graphql").
MatchType("json").
JSON(map[string]interface{}{
"query": "query ViewerProjects.*",
"variables": map[string]interface{}{
"after": nil, "afterFields": nil, "afterItems": nil, "first": 30, "firstFields": 100, "firstItems": 0,
},
}).
Reply(200).
JSON(map[string]interface{}{
"data": map[string]interface{}{
"viewer": map[string]interface{}{
"login": "monalisa",
"projectsV2": map[string]interface{}{
"totalCount": 1,
"nodes": []interface{}{
map[string]interface{}{
"id": "a-project-ID",
"number": 1,
"title": "@monalia's first project",
"url": "https://github.com/users/monalisa/projects/1",
},
},
},
},
},
})
},
promptStubs: func(pm *prompter.PrompterMock) {
pm.SelectFunc = func(prompt, _ string, opts []string) (int, error) {
switch prompt {
case "Which owner would you like to use?":
return prompter.IndexFor(opts, "monalisa")
case "Which project would you like to use?":
return prompter.IndexFor(opts, "@monalia's first project (#1)")
default:
return -1, prompter.NoSuchPromptErr(prompt)
}
}
},
expectedBrowse: "https://github.com/users/monalisa/projects/1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer gock.Off()
gock.Observe(gock.DumpRequest)
if tt.gqlStubs != nil {
tt.gqlStubs(t)
}
pm := &prompter.PrompterMock{}
if tt.promptStubs != nil {
tt.promptStubs(pm)
}
opts := viewOpts{}
if tt.setup != nil {
tt.setup(&opts, t)()
}
ios, _, _, _ := iostreams.Test()
ios.SetStdoutTTY(tt.tty)
ios.SetStdinTTY(tt.tty)
ios.SetStderrTTY(tt.tty)
buf := bytes.Buffer{}
client := queries.NewTestClient(queries.WithPrompter(pm))
config := viewConfig{
opts: opts,
URLOpener: func(url string) error {
_, err := buf.WriteString(url)
return err
},
client: client,
io: ios,
}
err := runView(config)
assert.NoError(t, err)
assert.Equal(t, tt.expectedBrowse, buf.String())
})
}
}