hugo/hugolib/menu_test.go
Bjørn Erik Pedersen a07293cf97 Create a Node map to get proper node translations
In a multi-language setup, before this commit the Node's Translations() method
would return some "dummy nodes" that would point to the correct page (Permalink),
but would not be the same as the node it points to -- it would not have the translated
title etc.

The node creation is, however, so mingled with rendering, whihc is too early to have any global state,
so the nodes has to be split in a prepare and a render phase. This commits does that with as small
a change as possible. This implementation is a temp solution until we fix #2297.

Updates #2309
2016-09-06 18:32:19 +03:00

705 lines
19 KiB
Go

// Copyright 2016 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hugolib
import (
"fmt"
"strings"
"testing"
"github.com/spf13/hugo/helpers"
"path/filepath"
toml "github.com/pelletier/go-toml"
"github.com/spf13/hugo/hugofs"
"github.com/spf13/hugo/source"
"github.com/spf13/viper"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const (
confMenu1 = `
[[menu.main]]
name = "Go Home"
url = "/"
weight = 1
pre = "<div>"
post = "</div>"
[[menu.main]]
name = "Blog"
url = "/posts"
[[menu.main]]
name = "ext"
url = "http://gohugo.io"
identifier = "ext"
[[menu.main]]
name = "ext2"
url = "http://foo.local/Zoo/foo"
identifier = "ext2"
[[menu.grandparent]]
name = "grandparent"
url = "/grandparent"
identifier = "grandparentId"
[[menu.grandparent]]
name = "parent"
url = "/parent"
identifier = "parentId"
parent = "grandparentId"
[[menu.grandparent]]
name = "Go Home3"
url = "/"
identifier = "grandchildId"
parent = "parentId"
[[menu.tax]]
name = "Tax1"
url = "/two/key/"
identifier="1"
[[menu.tax]]
name = "Tax2"
url = "/two/key/"
identifier="2"
[[menu.tax]]
name = "Tax RSS"
url = "/two/key.xml"
identifier="xml"
[[menu.hash]]
name = "Tax With #"
url = "/resource#anchor"
identifier="hash"
[[menu.unicode]]
name = "Unicode Russian"
identifier = "unicode-russian"
url = "/новости-проекта"` // Russian => "news-project"
)
var menuPage1 = []byte(`+++
title = "One"
[menu]
[menu.p_one]
weight = 1
+++
Front Matter with Menu Pages`)
var menuPage2 = []byte(`+++
title = "Two"
weight = 2
[menu]
[menu.p_one]
[menu.p_two]
identifier = "Two"
+++
Front Matter with Menu Pages`)
var menuPage3 = []byte(`+++
title = "Three"
weight = 3
[menu]
[menu.p_two]
Name = "Three"
Parent = "Two"
+++
Front Matter with Menu Pages`)
var menuPage4 = []byte(`+++
title = "Four"
weight = 4
[menu]
[menu.p_two]
Name = "Four"
Parent = "Three"
+++
Front Matter with Menu Pages`)
var menuPageSources = []source.ByteSource{
{Name: filepath.FromSlash("sect/doc1.md"), Content: menuPage1},
{Name: filepath.FromSlash("sect/doc2.md"), Content: menuPage2},
{Name: filepath.FromSlash("sect/doc3.md"), Content: menuPage3},
}
var menuPageSectionsSources = []source.ByteSource{
{Name: filepath.FromSlash("first/doc1.md"), Content: menuPage1},
{Name: filepath.FromSlash("first/doc2.md"), Content: menuPage2},
{Name: filepath.FromSlash("second-section/doc3.md"), Content: menuPage3},
{Name: filepath.FromSlash("Fish and Chips/doc4.md"), Content: menuPage4},
}
func tstCreateMenuPageWithNameTOML(title, menu, name string) []byte {
return []byte(fmt.Sprintf(`+++
title = "%s"
weight = 1
[menu]
[menu.%s]
name = "%s"
+++
Front Matter with Menu with Name`, title, menu, name))
}
func tstCreateMenuPageWithIdentifierTOML(title, menu, identifier string) []byte {
return []byte(fmt.Sprintf(`+++
title = "%s"
weight = 1
[menu]
[menu.%s]
identifier = "%s"
name = "somename"
+++
Front Matter with Menu with Identifier`, title, menu, identifier))
}
func tstCreateMenuPageWithNameYAML(title, menu, name string) []byte {
return []byte(fmt.Sprintf(`---
title: "%s"
weight: 1
menu:
%s:
name: "%s"
---
Front Matter with Menu with Name`, title, menu, name))
}
func tstCreateMenuPageWithIdentifierYAML(title, menu, identifier string) []byte {
return []byte(fmt.Sprintf(`---
title: "%s"
weight: 1
menu:
%s:
identifier: "%s"
name: "somename"
---
Front Matter with Menu with Identifier`, title, menu, identifier))
}
// Issue 817 - identifier should trump everything
func TestPageMenuWithIdentifier(t *testing.T) {
toml := []source.ByteSource{
{Name: "sect/doc1.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i1")},
{Name: "sect/doc2.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i2")},
{Name: "sect/doc3.md", Content: tstCreateMenuPageWithIdentifierTOML("t1", "m1", "i2")}, // duplicate
}
yaml := []source.ByteSource{
{Name: "sect/doc1.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i1")},
{Name: "sect/doc2.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i2")},
{Name: "sect/doc3.md", Content: tstCreateMenuPageWithIdentifierYAML("t1", "m1", "i2")}, // duplicate
}
doTestPageMenuWithIdentifier(t, toml)
doTestPageMenuWithIdentifier(t, yaml)
}
func doTestPageMenuWithIdentifier(t *testing.T, menuPageSources []source.ByteSource) {
testCommonResetState()
s := setupMenuTests(t, menuPageSources)
assert.Equal(t, 3, len(s.Pages), "Not enough pages")
me1 := findTestMenuEntryByID(s, "m1", "i1")
me2 := findTestMenuEntryByID(s, "m1", "i2")
assert.NotNil(t, me1)
assert.NotNil(t, me2)
assert.True(t, strings.Contains(me1.URL, "doc1"), me1.URL)
assert.True(t, strings.Contains(me2.URL, "doc2") || strings.Contains(me2.URL, "doc3"), me2.URL)
}
// Issue 817 contd - name should be second identifier in
func TestPageMenuWithDuplicateName(t *testing.T) {
toml := []source.ByteSource{
{Name: "sect/doc1.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n1")},
{Name: "sect/doc2.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n2")},
{Name: "sect/doc3.md", Content: tstCreateMenuPageWithNameTOML("t1", "m1", "n2")}, // duplicate
}
yaml := []source.ByteSource{
{Name: "sect/doc1.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n1")},
{Name: "sect/doc2.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n2")},
{Name: "sect/doc3.md", Content: tstCreateMenuPageWithNameYAML("t1", "m1", "n2")}, // duplicate
}
doTestPageMenuWithDuplicateName(t, toml)
doTestPageMenuWithDuplicateName(t, yaml)
}
func doTestPageMenuWithDuplicateName(t *testing.T, menuPageSources []source.ByteSource) {
testCommonResetState()
s := setupMenuTests(t, menuPageSources)
assert.Equal(t, 3, len(s.Pages), "Not enough pages")
me1 := findTestMenuEntryByName(s, "m1", "n1")
me2 := findTestMenuEntryByName(s, "m1", "n2")
assert.NotNil(t, me1)
assert.NotNil(t, me2)
assert.True(t, strings.Contains(me1.URL, "doc1"), me1.URL)
assert.True(t, strings.Contains(me2.URL, "doc2") || strings.Contains(me2.URL, "doc3"), me2.URL)
}
func TestPageMenu(t *testing.T) {
testCommonResetState()
s := setupMenuTests(t, menuPageSources)
if len(s.Pages) != 3 {
t.Fatalf("Posts not created, expected 3 got %d", len(s.Pages))
}
first := s.Pages[0]
second := s.Pages[1]
third := s.Pages[2]
pOne := findTestMenuEntryByName(s, "p_one", "One")
pTwo := findTestMenuEntryByID(s, "p_two", "Two")
for i, this := range []struct {
menu string
page *Page
menuItem *MenuEntry
isMenuCurrent bool
hasMenuCurrent bool
}{
{"p_one", first, pOne, true, false},
{"p_one", first, pTwo, false, false},
{"p_one", second, pTwo, false, false},
{"p_two", second, pTwo, true, false},
{"p_two", third, pTwo, false, true},
{"p_one", third, pTwo, false, false},
} {
isMenuCurrent := this.page.IsMenuCurrent(this.menu, this.menuItem)
hasMenuCurrent := this.page.HasMenuCurrent(this.menu, this.menuItem)
if isMenuCurrent != this.isMenuCurrent {
t.Errorf("[%d] Wrong result from IsMenuCurrent: %v", i, isMenuCurrent)
}
if hasMenuCurrent != this.hasMenuCurrent {
t.Errorf("[%d] Wrong result for menuItem %v for HasMenuCurrent: %v", i, this.menuItem, hasMenuCurrent)
}
}
}
func TestMenuURL(t *testing.T) {
testCommonResetState()
s := setupMenuTests(t, menuPageSources)
for i, this := range []struct {
me *MenuEntry
expectedURL string
}{
// issue #888
{findTestMenuEntryByID(s, "hash", "hash"), "/Zoo/resource#anchor"},
// issue #1774
{findTestMenuEntryByID(s, "main", "ext"), "http://gohugo.io"},
{findTestMenuEntryByID(s, "main", "ext2"), "http://foo.local/Zoo/foo"},
} {
if this.me == nil {
t.Errorf("[%d] MenuEntry not found", i)
continue
}
if this.me.URL != this.expectedURL {
t.Errorf("[%d] Got URL %s expected %s", i, this.me.URL, this.expectedURL)
}
}
}
// Issue #1934
func TestYAMLMenuWithMultipleEntries(t *testing.T) {
testCommonResetState()
ps1 := []byte(`---
title: "Yaml 1"
weight: 5
menu: ["p_one", "p_two"]
---
Yaml Front Matter with Menu Pages`)
ps2 := []byte(`---
title: "Yaml 2"
weight: 5
menu:
p_three:
p_four:
---
Yaml Front Matter with Menu Pages`)
s := setupMenuTests(t, []source.ByteSource{
{Name: filepath.FromSlash("sect/yaml1.md"), Content: ps1},
{Name: filepath.FromSlash("sect/yaml2.md"), Content: ps2}})
p1 := s.Pages[0]
assert.Len(t, p1.Menus(), 2, "List YAML")
p2 := s.Pages[1]
assert.Len(t, p2.Menus(), 2, "Map YAML")
}
// issue #719
func TestMenuWithUnicodeURLs(t *testing.T) {
for _, canonifyURLs := range []bool{true, false} {
doTestMenuWithUnicodeURLs(t, canonifyURLs)
}
}
func doTestMenuWithUnicodeURLs(t *testing.T, canonifyURLs bool) {
testCommonResetState()
viper.Set("CanonifyURLs", canonifyURLs)
s := setupMenuTests(t, menuPageSources)
unicodeRussian := findTestMenuEntryByID(s, "unicode", "unicode-russian")
expected := "/%D0%BD%D0%BE%D0%B2%D0%BE%D1%81%D1%82%D0%B8-%D0%BF%D1%80%D0%BE%D0%B5%D0%BA%D1%82%D0%B0"
if !canonifyURLs {
expected = "/Zoo" + expected
}
assert.Equal(t, expected, unicodeRussian.URL)
}
// Issue #1114
func TestSectionPagesMenu(t *testing.T) {
doTestSectionPagesMenu(true, t)
doTestSectionPagesMenu(false, t)
}
func doTestSectionPagesMenu(canonifyUrls bool, t *testing.T) {
testCommonResetState()
viper.Set("SectionPagesMenu", "spm")
viper.Set("CanonifyURLs", canonifyUrls)
s := setupMenuTests(t, menuPageSectionsSources)
assert.Equal(t, 3, len(s.Sections))
firstSectionPages := s.Sections["first"]
assert.Equal(t, 2, len(firstSectionPages))
secondSectionPages := s.Sections["second-section"]
assert.Equal(t, 1, len(secondSectionPages))
fishySectionPages := s.Sections["fish-and-chips"]
assert.Equal(t, 1, len(fishySectionPages))
nodeFirst := s.getNode("sect-first-0")
require.NotNil(t, nodeFirst)
nodeSecond := s.getNode("sect-second-section-0")
require.NotNil(t, nodeSecond)
nodeFishy := s.getNode("sect-Fish and Chips-0")
require.NotNil(t, nodeFishy)
firstSectionMenuEntry := findTestMenuEntryByID(s, "spm", "first")
secondSectionMenuEntry := findTestMenuEntryByID(s, "spm", "second-section")
fishySectionMenuEntry := findTestMenuEntryByID(s, "spm", "Fish and Chips")
assert.NotNil(t, firstSectionMenuEntry)
assert.NotNil(t, secondSectionMenuEntry)
assert.NotNil(t, nodeFirst)
assert.NotNil(t, nodeSecond)
assert.NotNil(t, fishySectionMenuEntry)
assert.NotNil(t, nodeFishy)
assert.True(t, nodeFirst.IsMenuCurrent("spm", firstSectionMenuEntry))
assert.False(t, nodeFirst.IsMenuCurrent("spm", secondSectionMenuEntry))
assert.False(t, nodeFirst.IsMenuCurrent("spm", fishySectionMenuEntry))
assert.True(t, nodeFishy.IsMenuCurrent("spm", fishySectionMenuEntry))
assert.Equal(t, "Fish and Chips", fishySectionMenuEntry.Name)
for _, p := range firstSectionPages {
assert.True(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
assert.False(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
}
for _, p := range secondSectionPages {
assert.False(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
assert.True(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
}
for _, p := range fishySectionPages {
assert.False(t, p.Page.HasMenuCurrent("spm", firstSectionMenuEntry))
assert.False(t, p.Page.HasMenuCurrent("spm", secondSectionMenuEntry))
assert.True(t, p.Page.HasMenuCurrent("spm", fishySectionMenuEntry))
}
}
func TestTaxonomyNodeMenu(t *testing.T) {
testCommonResetState()
viper.Set("CanonifyURLs", true)
s := setupMenuTests(t, menuPageSources)
for i, this := range []struct {
menu string
taxInfo taxRenderInfo
menuItem *MenuEntry
isMenuCurrent bool
hasMenuCurrent bool
}{
{"tax", taxRenderInfo{key: "key", singular: "one", plural: "two"},
findTestMenuEntryByID(s, "tax", "1"), true, false},
{"tax", taxRenderInfo{key: "key", singular: "one", plural: "two"},
findTestMenuEntryByID(s, "tax", "2"), true, false},
{"tax", taxRenderInfo{key: "key", singular: "one", plural: "two"},
&MenuEntry{Name: "Somewhere else", URL: "/somewhereelse"}, false, false},
} {
n, _ := s.newTaxonomyNode(true, this.taxInfo, i)
isMenuCurrent := n.IsMenuCurrent(this.menu, this.menuItem)
hasMenuCurrent := n.HasMenuCurrent(this.menu, this.menuItem)
if isMenuCurrent != this.isMenuCurrent {
t.Errorf("[%d] Wrong result from IsMenuCurrent: %v", i, isMenuCurrent)
}
if hasMenuCurrent != this.hasMenuCurrent {
t.Errorf("[%d] Wrong result for menuItem %v for HasMenuCurrent: %v", i, this.menuItem, hasMenuCurrent)
}
}
menuEntryXML := findTestMenuEntryByID(s, "tax", "xml")
if strings.HasSuffix(menuEntryXML.URL, "/") {
t.Error("RSS menu item should not be padded with trailing slash")
}
}
func TestMenuLimit(t *testing.T) {
testCommonResetState()
s := setupMenuTests(t, menuPageSources)
m := *s.Menus["main"]
// main menu has 4 entries
firstTwo := m.Limit(2)
assert.Equal(t, 2, len(firstTwo))
for i := 0; i < 2; i++ {
assert.Equal(t, m[i], firstTwo[i])
}
assert.Equal(t, m, m.Limit(4))
assert.Equal(t, m, m.Limit(5))
}
func TestMenuSortByN(t *testing.T) {
for i, this := range []struct {
sortFunc func(p Menu) Menu
assertFunc func(p Menu) bool
}{
{(Menu).Sort, func(p Menu) bool { return p[0].Weight == 1 && p[1].Name == "nx" && p[2].Identifier == "ib" }},
{(Menu).ByWeight, func(p Menu) bool { return p[0].Weight == 1 && p[1].Name == "nx" && p[2].Identifier == "ib" }},
{(Menu).ByName, func(p Menu) bool { return p[0].Name == "na" }},
{(Menu).Reverse, func(p Menu) bool { return p[0].Identifier == "ib" && p[len(p)-1].Identifier == "ia" }},
} {
menu := Menu{&MenuEntry{Weight: 3, Name: "nb", Identifier: "ia"},
&MenuEntry{Weight: 1, Name: "na", Identifier: "ic"},
&MenuEntry{Weight: 1, Name: "nx", Identifier: "ic"},
&MenuEntry{Weight: 2, Name: "nb", Identifier: "ix"},
&MenuEntry{Weight: 2, Name: "nb", Identifier: "ib"}}
sorted := this.sortFunc(menu)
if !this.assertFunc(sorted) {
t.Errorf("[%d] sort error", i)
}
}
}
func TestHomeNodeMenu(t *testing.T) {
testCommonResetState()
viper.Set("CanonifyURLs", true)
viper.Set("UglyURLs", true)
s := setupMenuTests(t, menuPageSources)
home := s.getNode("home-0")
homeMenuEntry := &MenuEntry{Name: home.Title, URL: home.URL()}
for i, this := range []struct {
menu string
menuItem *MenuEntry
isMenuCurrent bool
hasMenuCurrent bool
}{
{"main", homeMenuEntry, true, false},
{"doesnotexist", homeMenuEntry, false, false},
{"main", &MenuEntry{Name: "Somewhere else", URL: "/somewhereelse"}, false, false},
{"grandparent", findTestMenuEntryByID(s, "grandparent", "grandparentId"), false, true},
{"grandparent", findTestMenuEntryByID(s, "grandparent", "parentId"), false, true},
{"grandparent", findTestMenuEntryByID(s, "grandparent", "grandchildId"), true, false},
} {
isMenuCurrent := home.IsMenuCurrent(this.menu, this.menuItem)
hasMenuCurrent := home.HasMenuCurrent(this.menu, this.menuItem)
if isMenuCurrent != this.isMenuCurrent {
fmt.Println("isMenuCurrent", isMenuCurrent)
fmt.Printf("this: %#v\n", this)
t.Errorf("[%d] Wrong result from IsMenuCurrent: %v for %q", i, isMenuCurrent, this.menu)
}
if hasMenuCurrent != this.hasMenuCurrent {
fmt.Println("hasMenuCurrent", hasMenuCurrent)
fmt.Printf("this: %#v\n", this)
t.Errorf("[%d] Wrong result for menu %q menuItem %v for HasMenuCurrent: %v", i, this.menu, this.menuItem, hasMenuCurrent)
}
}
}
func TestHopefullyUniqueID(t *testing.T) {
assert.Equal(t, "i", (&MenuEntry{Identifier: "i", URL: "u", Name: "n"}).hopefullyUniqueID())
assert.Equal(t, "u", (&MenuEntry{Identifier: "", URL: "u", Name: "n"}).hopefullyUniqueID())
assert.Equal(t, "n", (&MenuEntry{Identifier: "", URL: "", Name: "n"}).hopefullyUniqueID())
}
func TestAddMenuEntryChild(t *testing.T) {
root := &MenuEntry{Weight: 1}
root.addChild(&MenuEntry{Weight: 2})
root.addChild(&MenuEntry{Weight: 1})
assert.Equal(t, 2, len(root.Children))
assert.Equal(t, 1, root.Children[0].Weight)
}
var testMenuIdentityMatcher = func(me *MenuEntry, id string) bool { return me.Identifier == id }
var testMenuNameMatcher = func(me *MenuEntry, id string) bool { return me.Name == id }
func findTestMenuEntryByID(s *Site, mn string, id string) *MenuEntry {
return findTestMenuEntry(s, mn, id, testMenuIdentityMatcher)
}
func findTestMenuEntryByName(s *Site, mn string, id string) *MenuEntry {
return findTestMenuEntry(s, mn, id, testMenuNameMatcher)
}
func findTestMenuEntry(s *Site, mn string, id string, matcher func(me *MenuEntry, id string) bool) *MenuEntry {
var found *MenuEntry
if menu, ok := s.Menus[mn]; ok {
for _, me := range *menu {
if matcher(me, id) {
if found != nil {
panic(fmt.Sprintf("Duplicate menu entry in menu %s with id/name %s", mn, id))
}
found = me
}
descendant := findDescendantTestMenuEntry(me, id, matcher)
if descendant != nil {
if found != nil {
panic(fmt.Sprintf("Duplicate menu entry in menu %s with id/name %s", mn, id))
}
found = descendant
}
}
}
return found
}
func findDescendantTestMenuEntry(parent *MenuEntry, id string, matcher func(me *MenuEntry, id string) bool) *MenuEntry {
var found *MenuEntry
if parent.HasChildren() {
for _, child := range parent.Children {
if matcher(child, id) {
if found != nil {
panic(fmt.Sprintf("Duplicate menu entry in menuitem %s with id/name %s", parent.KeyName(), id))
}
found = child
}
descendant := findDescendantTestMenuEntry(child, id, matcher)
if descendant != nil {
if found != nil {
panic(fmt.Sprintf("Duplicate menu entry in menuitem %s with id/name %s", parent.KeyName(), id))
}
found = descendant
}
}
}
return found
}
func setupTestMenuState(t *testing.T) {
menus, err := tomlToMap(confMenu1)
if err != nil {
t.Fatalf("Unable to Read menus: %v", err)
}
viper.Set("menu", menus["menu"])
viper.Set("baseurl", "http://foo.local/Zoo/")
}
func setupMenuTests(t *testing.T, pageSources []source.ByteSource) *Site {
s := createTestSite(pageSources)
setupTestMenuState(t)
testSiteSetup(s, t)
return s
}
func createTestSite(pageSources []source.ByteSource) *Site {
hugofs.InitMemFs()
return &Site{
Source: &source.InMemorySource{ByteSource: pageSources},
Language: helpers.NewDefaultLanguage(),
}
}
func testSiteSetup(s *Site, t *testing.T) {
if err := buildSiteSkipRender(s); err != nil {
t.Fatalf("Sites build failed: %s", err)
}
}
func tomlToMap(s string) (map[string]interface{}, error) {
tree, err := toml.Load(s)
if err != nil {
return nil, err
}
return tree.ToMap(), nil
}