bf9395e022
CI / license-header (push) Has been skipped
CI / e2e-dry-run (push) Has been skipped
CI / fast-gate (push) Failing after 0s
Test PR Label Logic / test-pr-labels (push) Failing after 1s
Skill Format Check / check-format (push) Failing after 2s
CI / security (push) Failing after 5s
CI / unit-test (push) Has been skipped
CI / lint (push) Has been skipped
CI / script-test (push) Has been skipped
CI / deterministic-gate (push) Has been skipped
CI / coverage (push) Has been skipped
CI / results (push) Has been cancelled
CI / deadcode (push) Has been cancelled
CI / e2e-live (push) Has been cancelled
333 lines
10 KiB
Go
333 lines
10 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package sheets
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
// TestSheetStructureShortcuts_DryRun covers all 8 shortcuts in
|
|
// lark_sheet_sheet_structure (sheet-info + 7 dim-*) and verifies that the
|
|
// CLI's A1-style --range / --position / --count flags map straight through
|
|
// to the tool's `range` / `position` / `count` fields (or are normalised
|
|
// per shortcut's wire shape).
|
|
func TestSheetStructureShortcuts_DryRun(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
sc common.Shortcut
|
|
args []string
|
|
toolName string
|
|
wantInput map[string]interface{}
|
|
}{
|
|
{
|
|
name: "+sheet-info with include single category → narrow info_type",
|
|
sc: SheetInfo,
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--include", "row_heights,col_widths"},
|
|
toolName: "get_sheet_structure",
|
|
wantInput: map[string]interface{}{
|
|
"excel_id": testToken,
|
|
"sheet_id": testSheetID,
|
|
"info_type": "row_heights_column_widths",
|
|
},
|
|
},
|
|
{
|
|
name: "+sheet-info with mixed include → all",
|
|
sc: SheetInfo,
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--include", "row_heights,merges"},
|
|
toolName: "get_sheet_structure",
|
|
wantInput: map[string]interface{}{
|
|
"excel_id": testToken,
|
|
"sheet_id": testSheetID,
|
|
"info_type": "all",
|
|
},
|
|
},
|
|
{
|
|
name: "+dim-insert row position=6 count=3 inherit-before",
|
|
sc: DimInsert,
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--position", "6", "--count", "3", "--inherit-style", "before"},
|
|
toolName: "modify_sheet_structure",
|
|
wantInput: map[string]interface{}{
|
|
"excel_id": testToken,
|
|
"operation": "insert",
|
|
"sheet_id": testSheetID,
|
|
"position": "6",
|
|
"count": float64(3),
|
|
"side": "before",
|
|
},
|
|
},
|
|
{
|
|
name: "+dim-insert column position=C count=2",
|
|
sc: DimInsert,
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--position", "C", "--count", "2"},
|
|
toolName: "modify_sheet_structure",
|
|
wantInput: map[string]interface{}{
|
|
"excel_id": testToken,
|
|
"operation": "insert",
|
|
"sheet_id": testSheetID,
|
|
"position": "C",
|
|
"count": float64(2),
|
|
},
|
|
},
|
|
{
|
|
name: "+dim-delete column B:D",
|
|
sc: DimDelete,
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "B:D"},
|
|
toolName: "modify_sheet_structure",
|
|
wantInput: map[string]interface{}{
|
|
"excel_id": testToken,
|
|
"operation": "delete",
|
|
"sheet_id": testSheetID,
|
|
"range": "B:D",
|
|
},
|
|
},
|
|
{
|
|
name: "+dim-hide row 3:5",
|
|
sc: DimHide,
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "3:5"},
|
|
toolName: "modify_sheet_structure",
|
|
wantInput: map[string]interface{}{
|
|
"excel_id": testToken,
|
|
"operation": "hide",
|
|
"sheet_id": testSheetID,
|
|
"range": "3:5",
|
|
},
|
|
},
|
|
{
|
|
name: "+dim-unhide column AA:AC",
|
|
sc: DimUnhide,
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "AA:AC"},
|
|
toolName: "modify_sheet_structure",
|
|
wantInput: map[string]interface{}{
|
|
"excel_id": testToken,
|
|
"operation": "unhide",
|
|
"sheet_id": testSheetID,
|
|
"range": "AA:AC",
|
|
},
|
|
},
|
|
{
|
|
name: "+dim-freeze row count=2 → freeze_rows",
|
|
sc: DimFreeze,
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--dimension", "row", "--count", "2"},
|
|
toolName: "modify_sheet_structure",
|
|
wantInput: map[string]interface{}{
|
|
"excel_id": testToken,
|
|
"operation": "freeze",
|
|
"sheet_id": testSheetID,
|
|
"freeze_rows": float64(2),
|
|
},
|
|
},
|
|
{
|
|
name: "+dim-freeze count=0 → unfreeze",
|
|
sc: DimFreeze,
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--dimension", "column", "--count", "0"},
|
|
toolName: "modify_sheet_structure",
|
|
wantInput: map[string]interface{}{
|
|
"excel_id": testToken,
|
|
"operation": "unfreeze",
|
|
"sheet_id": testSheetID,
|
|
},
|
|
},
|
|
{
|
|
name: "+dim-group row 1:5 fold",
|
|
sc: DimGroup,
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "1:5", "--group-state", "fold"},
|
|
toolName: "modify_sheet_structure",
|
|
wantInput: map[string]interface{}{
|
|
"excel_id": testToken,
|
|
"operation": "group",
|
|
"sheet_id": testSheetID,
|
|
"range": "1:5",
|
|
"group_state": "fold",
|
|
},
|
|
},
|
|
{
|
|
name: "+dim-ungroup row 1:5",
|
|
sc: DimUngroup,
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "1:5"},
|
|
toolName: "modify_sheet_structure",
|
|
wantInput: map[string]interface{}{
|
|
"excel_id": testToken,
|
|
"operation": "ungroup",
|
|
"sheet_id": testSheetID,
|
|
"range": "1:5",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
body := parseDryRunBody(t, tt.sc, tt.args)
|
|
got := decodeToolInput(t, body, tt.toolName)
|
|
assertInputEquals(t, got, tt.wantInput)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDimRange_Validation covers the A1 range parser's edge cases routed
|
|
// through +dim-hide (any --range shortcut works; we just need to exercise
|
|
// the validator).
|
|
func TestDimRange_Validation(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
name string
|
|
args []string
|
|
want string
|
|
}{
|
|
{
|
|
name: "end before start",
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "5:3", "--dry-run"},
|
|
want: "end position is before start",
|
|
},
|
|
{
|
|
name: "mix row+column",
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "3:C", "--dry-run"},
|
|
want: "cannot mix row",
|
|
},
|
|
{
|
|
name: "invalid characters",
|
|
args: []string{"--url", testURL, "--sheet-id", testSheetID, "--range", "A1:B2", "--dry-run"},
|
|
want: "expected pure digits",
|
|
},
|
|
}
|
|
for _, tt := range cases {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
_, _, err := runShortcutCapturingErr(t, DimHide, tt.args)
|
|
requireValidation(t, err, tt.want)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDimMove_DryRun verifies the native v3 move_dimension payload shape.
|
|
// CLI's --source-range "1:3" (1-based inclusive) is parsed into
|
|
// source.{start_index=0, end_index=2} (0-based inclusive), and sheet_id is
|
|
// carried in the path, not the body. --target "11" → destination_index=10.
|
|
func TestDimMove_DryRun(t *testing.T) {
|
|
t.Parallel()
|
|
calls := parseDryRunAPI(t, DimMove, []string{
|
|
"--url", testURL, "--sheet-id", testSheetID,
|
|
"--source-range", "1:3", "--target", "11",
|
|
})
|
|
if len(calls) != 1 {
|
|
t.Fatalf("api calls = %d, want 1", len(calls))
|
|
}
|
|
c := calls[0].(map[string]interface{})
|
|
wantURL := "/sheets/v3/spreadsheets/" + testToken + "/sheets/" + testSheetID + "/move_dimension"
|
|
if !strings.Contains(c["url"].(string), wantURL) {
|
|
t.Errorf("url = %v, want suffix %v", c["url"], wantURL)
|
|
}
|
|
body, _ := c["body"].(map[string]interface{})
|
|
src, _ := body["source"].(map[string]interface{})
|
|
if src["major_dimension"] != "ROWS" {
|
|
t.Errorf("source.major_dimension = %v, want ROWS", src["major_dimension"])
|
|
}
|
|
if src["start_index"].(float64) != 0 {
|
|
t.Errorf("start_index = %v, want 0", src["start_index"])
|
|
}
|
|
if src["end_index"].(float64) != 2 {
|
|
t.Errorf("end_index = %v, want 2 (0-based inclusive)", src["end_index"])
|
|
}
|
|
if body["destination_index"].(float64) != 10 {
|
|
t.Errorf("destination_index = %v, want 10 (target \"11\" → 0-based 10)", body["destination_index"])
|
|
}
|
|
}
|
|
|
|
// TestDimMove_Column exercises the column path: --source-range "C:F" →
|
|
// COLUMNS / start=2 / end=5; --target "H" → destination_index=7.
|
|
func TestDimMove_Column(t *testing.T) {
|
|
t.Parallel()
|
|
calls := parseDryRunAPI(t, DimMove, []string{
|
|
"--url", testURL, "--sheet-id", testSheetID,
|
|
"--source-range", "C:F", "--target", "H",
|
|
})
|
|
c := calls[0].(map[string]interface{})
|
|
body, _ := c["body"].(map[string]interface{})
|
|
src, _ := body["source"].(map[string]interface{})
|
|
if src["major_dimension"] != "COLUMNS" {
|
|
t.Errorf("major_dimension = %v, want COLUMNS", src["major_dimension"])
|
|
}
|
|
if src["start_index"].(float64) != 2 || src["end_index"].(float64) != 5 {
|
|
t.Errorf("source = %v, want start=2 end=5", src)
|
|
}
|
|
if body["destination_index"].(float64) != 7 {
|
|
t.Errorf("destination_index = %v, want 7", body["destination_index"])
|
|
}
|
|
}
|
|
|
|
// TestDimMove_MismatchedDimension verifies that mixing source row + target
|
|
// column (or vice versa) is rejected at Validate.
|
|
func TestDimMove_MismatchedDimension(t *testing.T) {
|
|
t.Parallel()
|
|
_, _, err := runShortcutCapturingErr(t, DimMove, []string{
|
|
"--url", testURL, "--sheet-id", testSheetID,
|
|
"--source-range", "1:3", "--target", "H", "--dry-run",
|
|
})
|
|
requireValidation(t, err, "must match --source-range")
|
|
}
|
|
|
|
// TestParseA1Range covers parser edge cases directly.
|
|
func TestParseA1Range(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
in string
|
|
dim string
|
|
start int
|
|
end int
|
|
wantErr bool
|
|
}{
|
|
{"3:7", "row", 2, 6, false},
|
|
{"5", "row", 4, 4, false},
|
|
{"C:F", "column", 2, 5, false},
|
|
{"C", "column", 2, 2, false},
|
|
{"aa:ac", "column", 26, 28, false}, // lower-case letters accepted
|
|
{"", "", 0, 0, true},
|
|
{"3:C", "", 0, 0, true},
|
|
{"7:3", "", 0, 0, true},
|
|
{"A1", "", 0, 0, true}, // cell ref, not a row/col range
|
|
{"3:5:7", "", 0, 0, true},
|
|
{"0", "", 0, 0, true}, // rows are 1-based
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.in, func(t *testing.T) {
|
|
t.Parallel()
|
|
dim, start, end, err := parseA1Range(c.in)
|
|
if c.wantErr {
|
|
if err == nil {
|
|
t.Errorf("parseA1Range(%q) = (%q, %d, %d, nil), want error", c.in, dim, start, end)
|
|
}
|
|
return
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("parseA1Range(%q) unexpected error: %v", c.in, err)
|
|
}
|
|
if dim != c.dim || start != c.start || end != c.end {
|
|
t.Errorf("parseA1Range(%q) = (%q, %d, %d), want (%q, %d, %d)", c.in, dim, start, end, c.dim, c.start, c.end)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestColumnIndexToLetter exercises the corner cases of the letter helper
|
|
// (still in use by lark_sheet_workbook.go for absolute column refs).
|
|
func TestColumnIndexToLetter(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
idx int
|
|
want string
|
|
}{
|
|
{0, "A"}, {25, "Z"}, {26, "AA"}, {27, "AB"}, {51, "AZ"},
|
|
{52, "BA"}, {701, "ZZ"}, {702, "AAA"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := columnIndexToLetter(c.idx); got != c.want {
|
|
t.Errorf("columnIndexToLetter(%d) = %q, want %q", c.idx, got, c.want)
|
|
}
|
|
}
|
|
}
|