| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package htmlcheck |
| |
| import ( |
| "bytes" |
| "errors" |
| "reflect" |
| "strings" |
| "testing" |
| |
| "golang.org/x/net/html" |
| ) |
| |
| func TestParse(t *testing.T) { |
| var testCases = []struct { |
| text string |
| wantSelector *selector |
| wantErr error |
| }{ |
| { |
| "a#id", |
| &selector{ |
| atoms: []selectorAtom{&elementSelector{"a"}, &idSelector{"id"}}, |
| }, |
| nil, |
| }, |
| { |
| "a", |
| &selector{atoms: []selectorAtom{&elementSelector{"a"}}}, |
| nil, |
| }, |
| { |
| "#id", |
| &selector{atoms: []selectorAtom{&idSelector{"id"}}}, |
| nil, |
| }, |
| { |
| ".class a", |
| &selector{ |
| atoms: []selectorAtom{&classSelector{"class"}}, |
| next: &selector{atoms: []selectorAtom{&elementSelector{"a"}}}, |
| }, |
| nil, |
| }, |
| { |
| `[attribute-name="value"] a`, |
| &selector{ |
| atoms: []selectorAtom{&attributeSelector{attribute: "attribute-name", value: "value"}}, |
| next: &selector{atoms: []selectorAtom{&elementSelector{"a"}}}, |
| }, |
| nil, |
| }, |
| { |
| `a[attribute-name="value"] a`, |
| &selector{ |
| atoms: []selectorAtom{&elementSelector{"a"}, &attributeSelector{attribute: "attribute-name", value: "value"}}, |
| next: &selector{atoms: []selectorAtom{&elementSelector{"a"}}}, |
| }, |
| nil, |
| }, |
| { |
| `.class1.class2`, |
| &selector{ |
| atoms: []selectorAtom{&classSelector{"class1"}, &classSelector{"class2"}}, |
| }, |
| nil, |
| }, |
| { |
| `a.class1.class2`, |
| &selector{ |
| atoms: []selectorAtom{&elementSelector{"a"}, &classSelector{"class1"}, &classSelector{"class2"}}, |
| }, |
| nil, |
| }, |
| { |
| ".", |
| nil, |
| errors.New("no class name after '.'"), |
| }, |
| { |
| "#.", |
| nil, |
| errors.New("no id name after '#'"), |
| }, |
| { |
| "[]", |
| nil, |
| errors.New("expected attribute name after '[' in attribute selector"), |
| }, |
| { |
| "[attribute-name]", |
| nil, |
| errors.New("expected '=' after attribute name in attribute selector"), |
| }, |
| { |
| "[attribute-name=]", |
| nil, |
| errors.New("expected '\"' after = in attribute selector"), |
| }, |
| { |
| `[attribute-name=""]`, |
| &selector{atoms: []selectorAtom{&attributeSelector{attribute: "attribute-name", value: ""}}}, |
| nil, |
| }, |
| { |
| `[attribute-name="]`, |
| nil, |
| errors.New("expected '\"' after attribute value"), |
| }, |
| { |
| `[attribute-name="value`, |
| nil, |
| errors.New("expected '\"' after attribute value"), |
| }, |
| { |
| `[attribute-name="value"`, |
| nil, |
| errors.New("expected ']' at end of attribute selector"), |
| }, |
| { |
| `.VulnMain-title:nth-of-type(4)`, |
| &selector{atoms: []selectorAtom{&classSelector{"VulnMain-title"}, &nthOfType{4}}}, |
| nil, |
| }, |
| { |
| `th:nth-child(2)`, |
| &selector{atoms: []selectorAtom{&elementSelector{"th"}, &nthChild{2}}}, |
| nil, |
| }, |
| { |
| `th:(2)`, |
| nil, |
| errors.New("expected identifier after : in pseudo class"), |
| }, |
| { |
| `th:32(2)`, |
| nil, |
| errors.New("expected identifier after : in pseudo class"), |
| }, |
| { |
| `th:active`, |
| nil, |
| errors.New("only :nth-of-type() and :nth-child() pseudoclasses are supported"), |
| }, |
| { |
| `th:nth-child`, |
| nil, |
| errors.New("expected '(' after :nth-of-type or nth-child"), |
| }, |
| { |
| `th:nth-child(odd)`, |
| nil, |
| errors.New("only number arguments are supported for :nth-of-type() or :nth-child()"), |
| }, |
| { |
| `th:nth-child(14`, |
| nil, |
| errors.New("expected ')' after number argument to nth-of-type or nth-child"), |
| }, |
| // We don't support the child combinator. Make sure it returns a parse error. |
| { |
| ".Documentation-sinceVersion > .Documentation-sinceVersionVersion", |
| nil, |
| errors.New("unexpected character '>' in input"), |
| }, |
| } |
| for _, tc := range testCases { |
| sel, err := parse(tc.text) |
| if tc.wantErr != nil { |
| if err == nil { |
| t.Fatalf("parse(%q): got nil err, want err %q", tc.text, tc.wantErr) |
| } |
| if tc.wantErr.Error() != err.Error() { |
| t.Fatalf("parse(%q): got err %q, want err %q", tc.text, err, tc.wantErr) |
| } |
| } else if err != nil { |
| t.Fatalf("parse(%q): got err %q, want nil error", tc.text, err) |
| } |
| if !reflect.DeepEqual(sel, tc.wantSelector) { |
| t.Fatalf("parse(%q): got %v; want %v", tc.text, sel, tc.wantSelector) |
| } |
| } |
| } |
| |
| func TestQuery(t *testing.T) { |
| var testCases = []struct { |
| queriedText string |
| selector string |
| want string |
| }{ |
| {"<a></a>", "a", "<a></a>"}, |
| {`<a></a><a id="id">text</a>`, "a#id", `<a id="id">text</a>`}, |
| {`<a></a><a class="class1"></a><a class="class2"></a><a class="class1 class2">text</a>`, ".class1.class2", `<a class="class1 class2">text</a>`}, |
| {`<a></a><a class="class1">first</a><a class="class2"></a><a class="class1 class2">text</a>`, ".class1", `<a class="class1">first</a>`}, |
| {`<div><div></div><div my-attr="my-val">text</div></div>`, `[my-attr="my-val"]`, `<div my-attr="my-val">text</div>`}, |
| {`<div><div></div><div my-attr="my-val">text</div></div>`, `[myattr="my-val"]`, ""}, |
| {`<html><head></head><body><div><div></div><div my-attr="my-val">text</div></div></body></html>`, ``, `<html><head></head><body><div><div></div><div my-attr="my-val">text</div></div></body></html>`}, |
| {`<div></div><div><div>match me</div></div>`, "div div", `<div>match me</div>`}, |
| {`<div></div><div><div>wrong</div></div><div id="wrong-id"><div class="my-class">also wrong</div></div><div id="my-id"><div class="wrong-class">still wrong</div></div><div id="my-id"><div class="my-class">match</div></div>`, "div#my-id div.my-class", `<div class="my-class">match</div>`}, |
| {`<a></a><div class="UnitMeta-repo"><a href="foo" title="">link body</a></div>`, ".UnitMeta-repo a", `<a href="foo" title="">link body</a>`}, |
| {`<ul class="UnitFiles-fileList"><li><a href="foo">a.go</a></li></ul>`, ".UnitFiles-fileList a", `<a href="foo">a.go</a>`}, |
| {`<ul><li>first child</li><li>second child</li></ul>`, "li:nth-child(2)", `<li>second child</li>`}, |
| {`<ul> <li>first child</li> <li>second child</li> </ul>`, "li:nth-child(2)", `<li>second child</li>`}, |
| {`<div><div>not counted</div><p class="class">first paragraph</p></div>`, ".class:nth-of-type(1)", `<p class="class">first paragraph</p>`}, |
| {`<div><div>not counted</div> <p class="class">first paragraph</p> </div>`, ".class:nth-of-type(1)", `<p class="class">first paragraph</p>`}, |
| {`<div><div class="class">not counted</div><p class="class">first paragraph</p>`, ".class:nth-of-type(2)", ``}, |
| {`<div><div class="class">not counted</div><p class="class">first paragraph</p><p class="class">second paragraph</p></div>`, ".class:nth-of-type(2)", `<p class="class">second paragraph</p>`}, |
| {`<div><div>not counted</div><p>first paragraph</p><p class="class">second paragraph</p></div>`, ".class:nth-of-type(2)", `<p class="class">second paragraph</p>`}, |
| {`<div><div>not counted</div><p>first paragraph</p><div>also not counted</div><p class="class">second paragraph</p></div>`, ".class:nth-of-type(2)", `<p class="class">second paragraph</p>`}, |
| {`<div><div>not counted</div><p>first paragraph</p><div>also not counted</div><p class="class">second paragraph</p><td>also not counted</td><p>third paragraph</p><p>fourth paragraph</p><p class="class">fifth paragraph</p></div>`, ".class:nth-of-type(5)", `<p class="class">fifth paragraph</p>`}, |
| {`<table class="UnitDirectories-table"><tbody><tr class="UnitDirectories-tableHeader"> <th>Path</th> <th class="UnitDirectories-desktopSynopsis">Synopsis</th></tr>`, "th:nth-child(1)", "<th>Path</th>"}, |
| {`<table class="UnitDirectories-table"> <tbody> <tr class="UnitDirectories-tableHeader"> <th>Path</th> <th class="UnitDirectories-desktopSynopsis"> Synopsis </th> </tr>`, "th:nth-child(1)", "<th>Path</th>"}, |
| } |
| for _, tc := range testCases { |
| n, err := html.Parse(strings.NewReader(tc.queriedText)) |
| if err != nil { |
| t.Fatalf("parsing queried text %q: %v", tc.queriedText, err) |
| } |
| sel, err := parse(tc.selector) |
| if err != nil { |
| t.Fatalf("parsing selector %q: %v", tc.selector, err) |
| } |
| got := query(n, sel) |
| if got == nil { |
| if tc.want == "" { |
| continue |
| } |
| t.Fatalf("query(%q, %q): got nil; want %q", tc.queriedText, tc.selector, tc.want) |
| } |
| var buf bytes.Buffer |
| err = html.Render(&buf, got) |
| if err != nil { |
| t.Fatalf("rendering result of query: %v", err) |
| } |
| if buf.String() != tc.want { |
| t.Fatalf("query(%q, %q): got %q; want %q", tc.queriedText, tc.selector, buf.String(), tc.want) |
| } |
| } |
| } |