package cascadia import ( "bytes" "encoding/json" "fmt" "io/ioutil" "log" "reflect" "strings" "testing" "golang.org/x/net/html" ) var validSelectors []validSelector func init() { c, err := ioutil.ReadFile("test_resources/valid_selectors.json") if err != nil { log.Fatal(err) } if err = json.Unmarshal(c, &validSelectors); err != nil { log.Fatal(err) } } type selectorTest struct { HTML, selector string results []string } func nodeString(n *html.Node) string { buf := bytes.NewBufferString("") if err := html.Render(buf, n); err != nil { log.Fatal(err) } return buf.String() } var selectorTests = []selectorTest{ { `
This address...`, "address", []string{ "This address...", }, }, { `text`, "*", []string{ "text", "", "text", }, }, { ``, "*", []string{ "", "", "", }, }, { ``, "#foo", []string{ `
`, }, }, { ``, "li#t1", []string{ `
`, "p.t1", []string{ `
`, }, }, { ``, ".t1.fail", []string{}, }, { `
`, "p.t1.t2", []string{ `
`, }, }, { ``, "p.--t1", []string{ `
`, }, }, { ``, "p.--t1.--t2", []string{ `
`, }, }, { ``, "p[title]", []string{ `
`, }, }, { ``, `p[title!="FooBarUFoo" i]`, []string{ `
`, }, }, { ``, `[ title ~= foo ]`, []string{ `
`, }, }, { ``, `p[title~="FOO" i]`, []string{ `
`, }, }, { ``, `p[title~=toofoo i]`, []string{}, }, { `
`, `[title~="hello world"]`, []string{}, }, { `
`, `[title~="hello" i]`, []string{ `
`, }, }, { ``, `[title~="hello" I]`, []string{ `
`, }, }, { ``, `[lang|="en"]`, []string{ `
`, ``, }, }, { ``, `[lang|="EN" i]`, []string{ `
`, ``, }, }, { ``, `[lang|="EN" i]`, []string{ `
`, ``, }, }, { ``, `[title^="foo"]`, []string{ `
`, }, }, { ``, `[title^="foo" i]`, []string{ `
`, }, }, { ``, `[title$="bar"]`, []string{ `
`, }, }, { ``, `[title$="BAR" i]`, []string{ `
`, }, }, { ``, `[title*="bar"]`, []string{ `
`, }, }, { ``, `[title*="BaRu" i]`, []string{ `
`, }, }, { ``, `[title*="BaRu" I]`, []string{ `
`, }, }, { `This text should be green.
This text should be green.
`, `p[class$=" "]`, []string{}, }, { `This text should be green.
This text should be green.
`, `p[class$=""]`, []string{}, }, { `This text should be green.
This text should be green.
`, `p[class^=" "]`, []string{}, }, { `This text should be green.
This text should be green.
`, `p[class^=""]`, []string{}, }, { `This text should be green.
This text should be green.
`, `p[class*=" "]`, []string{}, }, { `This text should be green.
This text should be green.
`, `p[class*=""]`, []string{}, }, { ``, `input[name=Sex][value=F]`, []string{ ``, }, }, { ``, ".t1:not(.t2)", []string{}, }, { `
some text and a span and another
`, `span:first-child`, []string{ `and a span`, }, }, { `a span and some text`, `span:last-child`, []string{ `a span`, }, }, { ``, `p:nth-of-type(2)`, []string{ `
`, }, }, { ``, `p:nth-last-of-type(2)`, []string{ ``, }, }, { ``, `p:last-of-type`, []string{ ``, }, }, { ``, `p:first-of-type`, []string{ ``, }, }, { `Hello
`,
`:empty`,
[]string{
``,
``,
``,
},
},
{
` `,
`div p`,
[]string{
`
`, `div table p`, []string{ `
`, }, }, { ``, `p ~ p`, []string{ `
`, ``, }, }, { ``, `p + p`, []string{ `
`, }, }, { ``, `li, p`, []string{ "
", "", "", }, }, { ``, `p +/*This is a comment*/ p`, []string{ `
`, }, }, { `Text block that wraps inner text and continues
`, `p:contains("that wraps")`, []string{ `Text block that wraps inner text and continues
`, }, }, { `Text block that wraps inner text and continues
`, `p:containsOwn("that wraps")`, []string{}, }, { `Text block that wraps inner text and continues
`, `:containsOwn("inner")`, []string{ `wraps inner text`, }, }, { `Text block that wraps inner text and continues
`, `p:containsOwn("block")`, []string{ `Text block that wraps inner text and continues
`, }, }, { `text content
text content
contents 1
contents 2
contents 2
contents 1
contents 2
contents 2
contents 2
`, }, }, { `contents 1
contents 2
contents 2
`, }, }, { `0123456789
abcdef
0123ABCD
`, `p:matches([\d])`, []string{ `0123456789
`, `0123ABCD
`, }, }, { `0123456789
abcdef
0123ABCD
`, `p:matches([a-z])`, []string{ `abcdef
`, }, }, { `0123456789
abcdef
0123ABCD
`, `p:matches([a-zA-Z])`, []string{ `abcdef
`, `0123ABCD
`, }, }, { `0123456789
abcdef
0123ABCD
`, `p:matches([^\d])`, []string{ `abcdef
`, `0123ABCD
`, }, }, { `0123456789
abcdef
0123ABCD
`, `p:matches(^(0|a))`, []string{ `0123456789
`, `abcdef
`, `0123ABCD
`, }, }, { `0123456789
abcdef
0123ABCD
`, `p:matches(^\d+$)`, []string{ `0123456789
`, }, }, { `0123456789
abcdef
0123ABCD
`, `p:not(:matches(^\d+$))`, []string{ `abcdef
`, `0123ABCD
`, }, }, { `0123456789
0123456789
`, `567`, }, }, { ``, `[href#=(fina)]:not([href#=(\/\/[^\/]+untrusted)])`, []string{ ``, ``, }, }, { ``, `[href#=(^https:\/\/[^\/]*\/?news)]`, []string{ ``, }, }, { ``, `:input`, []string{ ``, ``, ``, ``, ``, }, }, { ``, ":root", []string{ "", }, }, { ``, "*:root", []string{ "", }, }, { ``, "html:nth-child(1)", []string{ "", }, }, { ``, "*:root:first-child", []string{ ``, }, }, { ``, "*:root:nth-child(1)", []string{ ``, }, }, { ``, "a:not(:root)", []string{ ``, }, }, { ``, "body > *:nth-child(3n+2)", []string{ "", "", }, }, { ``, "input:disabled", []string{ ``, }, }, { ``, ":disabled", []string{ ``, }, }, { ``, ":enabled", []string{ ``, }, }, { ``, "div.class1, div.class2", []string{ ``, ``, }, }, } func setup(selector, testHTML string) (Selector, *html.Node, error) { s, err := Compile(selector) if err != nil { return nil, nil, fmt.Errorf("error compiling %q: %s", selector, err) } doc, err := html.Parse(strings.NewReader(testHTML)) if err != nil { return nil, nil, fmt.Errorf("error parsing %q: %s", testHTML, err) } return s, doc, nil } func TestSelectors(t *testing.T) { for _, test := range selectorTests { s, doc, err := setup(test.selector, test.HTML) if err != nil { t.Error(err) continue } matches := s.MatchAll(doc) if len(matches) != len(test.results) { t.Errorf("selector %s wanted %d elements, got %d instead", test.selector, len(test.results), len(matches)) continue } for i, m := range matches { got := nodeString(m) if got != test.results[i] { t.Errorf("selector %s wanted %s, got %s instead", test.selector, test.results[i], got) } } firstMatch := s.MatchFirst(doc) if len(test.results) == 0 { if firstMatch != nil { t.Errorf("MatchFirst: selector %s want nil, got %s", test.selector, nodeString(firstMatch)) } } else { got := nodeString(firstMatch) if got != test.results[0] { t.Errorf("MatchFirst: selector %s want %s, got %s", test.selector, test.results[0], got) } } } } func setupMatcher(selector, testHTML string) (Matcher, *html.Node, error) { s, err := ParseGroup(selector) if err != nil { return nil, nil, fmt.Errorf("error compiling %q: %s", selector, err) } doc, err := html.Parse(strings.NewReader(testHTML)) if err != nil { return nil, nil, fmt.Errorf("error parsing %q: %s", testHTML, err) } return s, doc, nil } func TestMatchers(t *testing.T) { for _, test := range selectorTests { s, doc, err := setupMatcher(test.selector, test.HTML) if err != nil { t.Error(err) continue } matches := QueryAll(doc, s) if len(matches) != len(test.results) { t.Errorf("selector %s wanted %d elements, got %d instead", test.selector, len(test.results), len(matches)) continue } for i, m := range matches { got := nodeString(m) if got != test.results[i] { t.Errorf("selector %s wanted %s, got %s instead", test.selector, test.results[i], got) } } firstMatch := Query(doc, s) if len(test.results) == 0 { if firstMatch != nil { t.Errorf("Query: selector %s want nil, got %s", test.selector, nodeString(firstMatch)) } } else { got := nodeString(firstMatch) if got != test.results[0] { t.Errorf("Query: selector %s want %s, got %s", test.selector, test.results[0], got) } } if !reflect.DeepEqual(matches, Selector(s.Match).Filter(matches)) { t.Fatalf("inconsistent Filter result") } } } type testPseudo struct { HTML, selector string spec Specificity pseudo string } var testsPseudo = []testPseudo{ { HTML: `