WitNote / internal /semantic /semantic_test.go
AUXteam's picture
Upload folder using huggingface_hub
6a7089a verified
package semantic
import (
"context"
"fmt"
"math"
"testing"
)
// ===========================================================================
// ElementDescriptor tests
// ===========================================================================
func TestComposite(t *testing.T) {
tests := []struct {
name string
desc ElementDescriptor
want string
}{
{
name: "role and name",
desc: ElementDescriptor{Ref: "e0", Role: "button", Name: "Submit"},
want: "button: Submit",
},
{
name: "role name and value",
desc: ElementDescriptor{Ref: "e1", Role: "textbox", Name: "Email", Value: "user@pinchtab.com"},
want: "textbox: Email [user@pinchtab.com]",
},
{
name: "name only",
desc: ElementDescriptor{Ref: "e2", Name: "Heading"},
want: "Heading",
},
{
name: "empty",
desc: ElementDescriptor{Ref: "e3"},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.desc.Composite()
if got != tt.want {
t.Errorf("Composite() = %q, want %q", got, tt.want)
}
})
}
}
// ===========================================================================
// CalibrateConfidence tests
// ===========================================================================
func TestCalibrateConfidence(t *testing.T) {
cases := []struct {
score float64
want string
}{
{1.0, "high"},
{0.85, "high"},
{0.8, "high"},
{0.79, "medium"},
{0.6, "medium"},
{0.59, "low"},
{0.0, "low"},
}
for _, c := range cases {
got := CalibrateConfidence(c.score)
if got != c.want {
t.Errorf("CalibrateConfidence(%f) = %q, want %q", c.score, got, c.want)
}
}
}
// ===========================================================================
// Stopword tests
// ===========================================================================
func TestIsStopword(t *testing.T) {
if !isStopword("the") {
t.Error("expected 'the' to be a stopword")
}
if isStopword("button") {
t.Error("expected 'button' not to be a stopword")
}
}
func TestRemoveStopwords(t *testing.T) {
tokens := []string{"click", "the", "submit", "button"}
filtered := removeStopwords(tokens)
if len(filtered) != 3 {
t.Errorf("expected 3 tokens after stopword removal, got %d: %v", len(filtered), filtered)
}
// When ALL tokens are stopwords, the original should be preserved.
allStop := []string{"the", "a", "is", "was"}
kept := removeStopwords(allStop)
if len(kept) != len(allStop) {
t.Errorf("expected original tokens when all are stopwords, got %d", len(kept))
}
}
// ===========================================================================
// LexicalScore tests
// ===========================================================================
func TestLexicalScore_ExactMatch(t *testing.T) {
score := LexicalScore("submit button", "button: Submit")
if score < 0.5 {
t.Errorf("expected high score for exact match, got %f", score)
}
}
func TestLexicalScore_NoOverlap(t *testing.T) {
score := LexicalScore("download pdf", "button: Login")
if score > 0.3 {
t.Errorf("expected low score for no overlap, got %f", score)
}
}
func TestLexicalScore_RoleBoost(t *testing.T) {
// "button" is a role keyword; if it appears in both, a boost is applied.
withRole := LexicalScore("submit button", "button: Submit")
withoutRole := LexicalScore("submit action", "link: Submit")
if withRole <= withoutRole {
t.Errorf("expected role boost to increase score: withRole=%f, withoutRole=%f", withRole, withoutRole)
}
}
func TestLexicalScore_StopwordRemoval(t *testing.T) {
// "the" is a stopword — it should be removed so both queries score similarly.
s1 := LexicalScore("click the button", "button: Click")
s2 := LexicalScore("click button", "button: Click")
diff := math.Abs(s1 - s2)
if diff > 0.01 {
t.Errorf("stopwords should not affect score significantly: s1=%f, s2=%f, diff=%f", s1, s2, diff)
}
}
// ===========================================================================
// LexicalMatcher (ElementMatcher interface) tests
// ===========================================================================
func TestLexicalMatcher_Find(t *testing.T) {
m := NewLexicalMatcher()
if m.Strategy() != "lexical" {
t.Errorf("expected strategy=lexical, got %s", m.Strategy())
}
elements := []ElementDescriptor{
{Ref: "e0", Role: "button", Name: "Log In"},
{Ref: "e1", Role: "link", Name: "Sign Up"},
{Ref: "e2", Role: "textbox", Name: "Email Address"},
}
result, err := m.Find(context.Background(), "log in button", elements, FindOptions{
Threshold: 0.1,
TopK: 3,
})
if err != nil {
t.Fatalf("Find returned error: %v", err)
}
if result.ElementCount != 3 {
t.Errorf("expected ElementCount=3, got %d", result.ElementCount)
}
if result.BestRef != "e0" {
t.Errorf("expected BestRef=e0, got %s", result.BestRef)
}
if result.BestScore <= 0 {
t.Errorf("expected positive BestScore, got %f", result.BestScore)
}
}
func TestLexicalMatcher_ThresholdFiltering(t *testing.T) {
m := NewLexicalMatcher()
elements := []ElementDescriptor{
{Ref: "e0", Role: "button", Name: "Submit"},
{Ref: "e1", Role: "link", Name: "Home"},
}
result, err := m.Find(context.Background(), "submit button", elements, FindOptions{
Threshold: 0.99,
TopK: 5,
})
if err != nil {
t.Fatalf("Find returned error: %v", err)
}
// Very high threshold — most likely nothing passes.
for _, m := range result.Matches {
if m.Score < 0.99 {
t.Errorf("match %s has score %f below threshold", m.Ref, m.Score)
}
}
}
// ===========================================================================
// DummyEmbedder tests
// ===========================================================================
func TestDummyEmbedder_Deterministic(t *testing.T) {
e := NewDummyEmbedder(64)
v1, err := e.Embed([]string{"hello world"})
if err != nil {
t.Fatalf("Embed error: %v", err)
}
v2, err := e.Embed([]string{"hello world"})
if err != nil {
t.Fatalf("Embed error: %v", err)
}
if len(v1[0]) != 64 {
t.Errorf("expected dim=64, got %d", len(v1[0]))
}
for i := range v1[0] {
if v1[0][i] != v2[0][i] {
t.Fatalf("DummyEmbedder is not deterministic at dim %d", i)
}
}
}
func TestDummyEmbedder_Strategy(t *testing.T) {
e := NewDummyEmbedder(32)
if e.Strategy() != "dummy" {
t.Errorf("expected strategy=dummy, got %s", e.Strategy())
}
}
func TestDummyEmbedder_DefaultDim(t *testing.T) {
e := NewDummyEmbedder(0)
if e.Dim != 64 {
t.Errorf("expected default dim=64, got %d", e.Dim)
}
}
func TestDummyEmbedder_NormalizedOutput(t *testing.T) {
e := NewDummyEmbedder(64)
vecs, err := e.Embed([]string{"test string"})
if err != nil {
t.Fatalf("Embed error: %v", err)
}
var norm float64
for _, v := range vecs[0] {
norm += float64(v) * float64(v)
}
norm = math.Sqrt(norm)
if math.Abs(norm-1.0) > 0.01 {
t.Errorf("expected unit-norm vector, got norm=%f", norm)
}
}
// ===========================================================================
// CosineSimilarity tests
// ===========================================================================
func TestCosineSimilarity_Identical(t *testing.T) {
v := []float32{1, 0, 0, 0}
sim := CosineSimilarity(v, v)
if math.Abs(sim-1.0) > 1e-6 {
t.Errorf("identical vectors should have similarity 1.0, got %f", sim)
}
}
func TestCosineSimilarity_Orthogonal(t *testing.T) {
a := []float32{1, 0, 0, 0}
b := []float32{0, 1, 0, 0}
sim := CosineSimilarity(a, b)
if math.Abs(sim) > 1e-6 {
t.Errorf("orthogonal vectors should have similarity ~0, got %f", sim)
}
}
func TestCosineSimilarity_Empty(t *testing.T) {
sim := CosineSimilarity(nil, nil)
if sim != 0 {
t.Errorf("empty vectors should have similarity 0, got %f", sim)
}
}
func TestCosineSimilarity_DifferentLengths(t *testing.T) {
a := []float32{1, 0}
b := []float32{1, 0, 0}
sim := CosineSimilarity(a, b)
if sim != 0 {
t.Errorf("different-length vectors should return 0, got %f", sim)
}
}
// ===========================================================================
// EmbeddingMatcher tests
// ===========================================================================
func TestEmbeddingMatcher_Strategy(t *testing.T) {
m := NewEmbeddingMatcher(NewDummyEmbedder(64))
want := "embedding:dummy"
if m.Strategy() != want {
t.Errorf("expected strategy=%s, got %s", want, m.Strategy())
}
}
func TestEmbeddingMatcher_Find(t *testing.T) {
m := NewEmbeddingMatcher(NewDummyEmbedder(64))
elements := []ElementDescriptor{
{Ref: "e0", Role: "button", Name: "Login"},
{Ref: "e1", Role: "textbox", Name: "Username"},
{Ref: "e2", Role: "link", Name: "Forgot Password"},
}
result, err := m.Find(context.Background(), "login button", elements, FindOptions{
Threshold: 0.0,
TopK: 3,
})
if err != nil {
t.Fatalf("Find returned error: %v", err)
}
if result.ElementCount != 3 {
t.Errorf("expected ElementCount=3, got %d", result.ElementCount)
}
if result.Strategy != "embedding:dummy" {
t.Errorf("expected strategy=embedding:dummy, got %s", result.Strategy)
}
if len(result.Matches) == 0 {
t.Error("expected at least one match")
}
// BestScore should be in valid range
if result.BestScore < 0 || result.BestScore > 1 {
t.Errorf("BestScore out of [0,1] range: %f", result.BestScore)
}
}
func TestEmbeddingMatcher_ThresholdFiltering(t *testing.T) {
m := NewEmbeddingMatcher(NewDummyEmbedder(64))
elements := []ElementDescriptor{
{Ref: "e0", Role: "button", Name: "Submit"},
{Ref: "e1", Role: "link", Name: "Cancel"},
}
result, err := m.Find(context.Background(), "xyz completely unrelated", elements, FindOptions{
Threshold: 0.99,
TopK: 5,
})
if err != nil {
t.Fatalf("Find returned error: %v", err)
}
for _, m := range result.Matches {
if m.Score < 0.99 {
t.Errorf("match %s score %f below threshold 0.99", m.Ref, m.Score)
}
}
}
// ===========================================================================
// FindResult.ConfidenceLabel tests
// ===========================================================================
func TestFindResult_ConfidenceLabel(t *testing.T) {
r := &FindResult{BestScore: 0.9}
if r.ConfidenceLabel() != "high" {
t.Errorf("expected high, got %s", r.ConfidenceLabel())
}
r.BestScore = 0.65
if r.ConfidenceLabel() != "medium" {
t.Errorf("expected medium, got %s", r.ConfidenceLabel())
}
r.BestScore = 0.1
if r.ConfidenceLabel() != "low" {
t.Errorf("expected low, got %s", r.ConfidenceLabel())
}
}
// ===========================================================================
// Phase 3: HashingEmbedder tests
// ===========================================================================
func TestHashingEmbedder_Strategy(t *testing.T) {
e := NewHashingEmbedder(128)
if e.Strategy() != "hashing" {
t.Errorf("expected strategy=hashing, got %s", e.Strategy())
}
}
func TestHashingEmbedder_DefaultDim(t *testing.T) {
e := NewHashingEmbedder(0)
if e.dim != 128 {
t.Errorf("expected default dim=128, got %d", e.dim)
}
}
func TestHashingEmbedder_Deterministic(t *testing.T) {
e := NewHashingEmbedder(128)
v1, err := e.Embed([]string{"click the submit button"})
if err != nil {
t.Fatalf("Embed error: %v", err)
}
v2, err := e.Embed([]string{"click the submit button"})
if err != nil {
t.Fatalf("Embed error: %v", err)
}
if len(v1[0]) != 128 {
t.Errorf("expected dim=128, got %d", len(v1[0]))
}
for i := range v1[0] {
if v1[0][i] != v2[0][i] {
t.Fatalf("HashingEmbedder not deterministic at dim %d: %f != %f", i, v1[0][i], v2[0][i])
}
}
}
func TestHashingEmbedder_Normalized(t *testing.T) {
e := NewHashingEmbedder(128)
vecs, err := e.Embed([]string{"button submit", "textbox username"})
if err != nil {
t.Fatalf("Embed error: %v", err)
}
for i, vec := range vecs {
var norm float64
for _, v := range vec {
norm += float64(v) * float64(v)
}
norm = math.Sqrt(norm)
if math.Abs(norm-1.0) > 0.01 {
t.Errorf("vector %d not unit-norm: norm=%f", i, norm)
}
}
}
func TestHashingEmbedder_EmptyInput(t *testing.T) {
e := NewHashingEmbedder(64)
vecs, err := e.Embed([]string{""})
if err != nil {
t.Fatalf("Embed error: %v", err)
}
if len(vecs[0]) != 64 {
t.Errorf("expected dim=64, got %d", len(vecs[0]))
}
// Empty string should produce a zero vector (no features to hash).
var sum float64
for _, v := range vecs[0] {
sum += float64(v) * float64(v)
}
if sum > 0 {
t.Error("empty input should produce zero vector")
}
}
func TestHashingEmbedder_SimilarTexts(t *testing.T) {
e := NewHashingEmbedder(256) // higher dim for less collision
vecs, err := e.Embed([]string{
"submit button", // 0
"submit form", // 1 – shares "submit"
"download report", // 2 – unrelated
})
if err != nil {
t.Fatalf("Embed error: %v", err)
}
simSameWord := CosineSimilarity(vecs[0], vecs[1]) // share "submit"
simUnrelated := CosineSimilarity(vecs[0], vecs[2]) // no shared words
if simSameWord <= simUnrelated {
t.Errorf("texts sharing 'submit' should be more similar: same=%f, unrelated=%f",
simSameWord, simUnrelated)
}
}
func TestHashingEmbedder_SubwordSimilarity(t *testing.T) {
e := NewHashingEmbedder(256)
// Character n-grams should give nonzero similarity between "button" and "btn".
vecs, err := e.Embed([]string{
"button", // full word
"btn", // abbreviation – shares "bt" bigram via n-grams
"search", // unrelated
})
if err != nil {
t.Fatalf("Embed error: %v", err)
}
simAbbrev := CosineSimilarity(vecs[0], vecs[1])
simUnrelated := CosineSimilarity(vecs[0], vecs[2])
// The abbreviation similarity might be small, but should be greater
// than an unrelated word due to shared character n-grams.
if simAbbrev <= simUnrelated {
t.Errorf("abbreviation should be more similar: abbrev=%f, unrelated=%f",
simAbbrev, simUnrelated)
}
}
func TestHashingEmbedder_RoleFeatures(t *testing.T) {
e := NewHashingEmbedder(128)
// "button" is a role keyword; it should get an extra role feature.
vecs, err := e.Embed([]string{
"button submit",
"button cancel",
"textbox email",
})
if err != nil {
t.Fatalf("Embed error: %v", err)
}
// Both have "button" role features — should be more similar to each other
// than to "textbox email" which has a different role keyword.
simSameRole := CosineSimilarity(vecs[0], vecs[1])
simDiffRole := CosineSimilarity(vecs[0], vecs[2])
if simSameRole <= simDiffRole {
t.Errorf("same-role elements should be more similar: same=%f, diff=%f",
simSameRole, simDiffRole)
}
}
func TestHashingEmbedder_BatchConsistency(t *testing.T) {
e := NewHashingEmbedder(128)
texts := []string{"login button", "search box", "navigation menu"}
batchVecs, err := e.Embed(texts)
if err != nil {
t.Fatalf("batch embed error: %v", err)
}
// Each text embedded individually should match the batch result.
for i, text := range texts {
singleVecs, err := e.Embed([]string{text})
if err != nil {
t.Fatalf("single embed error: %v", err)
}
for j := range singleVecs[0] {
if singleVecs[0][j] != batchVecs[i][j] {
t.Errorf("batch[%d] != single at dim %d: %f != %f", i, j, batchVecs[i][j], singleVecs[0][j])
break
}
}
}
}
// ===========================================================================
// Phase 3: CombinedMatcher tests
// ===========================================================================
func TestCombinedMatcher_Strategy(t *testing.T) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
want := "combined:lexical+embedding:hashing"
if m.Strategy() != want {
t.Errorf("expected strategy=%s, got %s", want, m.Strategy())
}
}
func TestCombinedMatcher_Find(t *testing.T) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
elements := []ElementDescriptor{
{Ref: "e0", Role: "button", Name: "Log In"},
{Ref: "e1", Role: "link", Name: "Sign Up"},
{Ref: "e2", Role: "textbox", Name: "Email Address"},
}
result, err := m.Find(context.Background(), "log in button", elements, FindOptions{
Threshold: 0.1,
TopK: 3,
})
if err != nil {
t.Fatalf("Find returned error: %v", err)
}
if result.ElementCount != 3 {
t.Errorf("expected ElementCount=3, got %d", result.ElementCount)
}
if result.BestRef != "e0" {
t.Errorf("expected BestRef=e0, got %s", result.BestRef)
}
if result.BestScore <= 0 {
t.Errorf("expected positive BestScore, got %f", result.BestScore)
}
if result.Strategy != "combined:lexical+embedding:hashing" {
t.Errorf("expected combined strategy, got %s", result.Strategy)
}
}
func TestCombinedMatcher_ThresholdFiltering(t *testing.T) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
elements := []ElementDescriptor{
{Ref: "e0", Role: "button", Name: "Submit"},
{Ref: "e1", Role: "link", Name: "Home"},
}
result, err := m.Find(context.Background(), "submit button", elements, FindOptions{
Threshold: 0.99,
TopK: 5,
})
if err != nil {
t.Fatalf("Find returned error: %v", err)
}
for _, match := range result.Matches {
if match.Score < 0.99 {
t.Errorf("match %s has score %f below threshold", match.Ref, match.Score)
}
}
}
func TestCombinedMatcher_TopK(t *testing.T) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
elements := []ElementDescriptor{
{Ref: "e0", Role: "button", Name: "Submit"},
{Ref: "e1", Role: "button", Name: "Cancel"},
{Ref: "e2", Role: "button", Name: "Reset"},
{Ref: "e3", Role: "link", Name: "Home"},
{Ref: "e4", Role: "textbox", Name: "Name"},
}
result, err := m.Find(context.Background(), "button", elements, FindOptions{
Threshold: 0.01,
TopK: 2,
})
if err != nil {
t.Fatalf("Find returned error: %v", err)
}
if len(result.Matches) > 2 {
t.Errorf("expected at most 2 matches (TopK=2), got %d", len(result.Matches))
}
}
func TestCombinedMatcher_ScoresDescending(t *testing.T) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
elements := []ElementDescriptor{
{Ref: "e0", Role: "button", Name: "Login"},
{Ref: "e1", Role: "textbox", Name: "Username"},
{Ref: "e2", Role: "link", Name: "Forgot Password"},
{Ref: "e3", Role: "heading", Name: "Welcome Page"},
}
result, err := m.Find(context.Background(), "login button", elements, FindOptions{
Threshold: 0.01,
TopK: 10,
})
if err != nil {
t.Fatalf("Find returned error: %v", err)
}
for i := 1; i < len(result.Matches); i++ {
if result.Matches[i].Score > result.Matches[i-1].Score {
t.Errorf("matches not sorted descending: [%d]=%f > [%d]=%f",
i, result.Matches[i].Score, i-1, result.Matches[i-1].Score)
}
}
}
func TestCombinedMatcher_WeightsApplied(t *testing.T) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
// Override weights to emphasize embedding.
m.LexicalWeight = 0.2
m.EmbeddingWeight = 0.8
elements := []ElementDescriptor{
{Ref: "e0", Role: "button", Name: "Log In"},
{Ref: "e1", Role: "link", Name: "Sign Up"},
}
result, err := m.Find(context.Background(), "log in", elements, FindOptions{
Threshold: 0.01,
TopK: 3,
})
if err != nil {
t.Fatalf("Find returned error: %v", err)
}
// With embedding-heavy weights the score should differ from default.
// Just verify the matcher runs and returns valid results.
if result.ElementCount != 2 {
t.Errorf("expected ElementCount=2, got %d", result.ElementCount)
}
if result.BestRef != "e0" {
t.Errorf("expected BestRef=e0, got %s", result.BestRef)
}
}
func TestCombinedMatcher_NoElements(t *testing.T) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
result, err := m.Find(context.Background(), "anything", nil, FindOptions{
Threshold: 0.1,
TopK: 3,
})
if err != nil {
t.Fatalf("Find returned error: %v", err)
}
if len(result.Matches) != 0 {
t.Errorf("expected no matches for empty elements, got %d", len(result.Matches))
}
if result.BestRef != "" {
t.Errorf("expected empty BestRef, got %s", result.BestRef)
}
}
// ===========================================================================
// Phase 3: Complex UI test scenarios
// ===========================================================================
// complexFormElements returns a realistic form page with 15+ elements.
func complexFormElements() []ElementDescriptor {
return []ElementDescriptor{
{Ref: "e0", Role: "heading", Name: "Registration Form"},
{Ref: "e1", Role: "textbox", Name: "First Name"},
{Ref: "e2", Role: "textbox", Name: "Last Name"},
{Ref: "e3", Role: "textbox", Name: "Email Address"},
{Ref: "e4", Role: "textbox", Name: "Password", Value: ""},
{Ref: "e5", Role: "textbox", Name: "Confirm Password"},
{Ref: "e6", Role: "combobox", Name: "Country"},
{Ref: "e7", Role: "checkbox", Name: "I agree to the Terms of Service"},
{Ref: "e8", Role: "checkbox", Name: "Subscribe to newsletter"},
{Ref: "e9", Role: "button", Name: "Submit Registration"},
{Ref: "e10", Role: "button", Name: "Cancel"},
{Ref: "e11", Role: "link", Name: "Already have an account? Log in"},
{Ref: "e12", Role: "link", Name: "Privacy Policy"},
{Ref: "e13", Role: "link", Name: "Terms of Service"},
{Ref: "e14", Role: "img", Name: "Company Logo"},
{Ref: "e15", Role: "navigation", Name: "Main Navigation"},
}
}
// complexTableElements returns a data table with columns and actions.
func complexTableElements() []ElementDescriptor {
return []ElementDescriptor{
{Ref: "e0", Role: "heading", Name: "User Management"},
{Ref: "e1", Role: "search", Name: "Search Users"},
{Ref: "e2", Role: "button", Name: "Add New User"},
{Ref: "e3", Role: "button", Name: "Export CSV"},
{Ref: "e4", Role: "table", Name: "Users Table"},
{Ref: "e5", Role: "columnheader", Name: "Name"},
{Ref: "e6", Role: "columnheader", Name: "Email"},
{Ref: "e7", Role: "columnheader", Name: "Role"},
{Ref: "e8", Role: "columnheader", Name: "Status"},
{Ref: "e9", Role: "columnheader", Name: "Actions"},
{Ref: "e10", Role: "cell", Name: "John Doe", Value: "john@pinchtab.com"},
{Ref: "e11", Role: "button", Name: "Edit", Value: "John Doe"},
{Ref: "e12", Role: "button", Name: "Delete", Value: "John Doe"},
{Ref: "e13", Role: "cell", Name: "Jane Smith", Value: "jane@pinchtab.com"},
{Ref: "e14", Role: "button", Name: "Edit", Value: "Jane Smith"},
{Ref: "e15", Role: "button", Name: "Delete", Value: "Jane Smith"},
{Ref: "e16", Role: "button", Name: "Previous Page"},
{Ref: "e17", Role: "button", Name: "Next Page"},
{Ref: "e18", Role: "combobox", Name: "Rows per page", Value: "10"},
}
}
// complexModalElements returns a page with a modal dialog overlay.
func complexModalElements() []ElementDescriptor {
return []ElementDescriptor{
{Ref: "e0", Role: "heading", Name: "Dashboard"},
{Ref: "e1", Role: "button", Name: "Settings"},
{Ref: "e2", Role: "button", Name: "Notifications"},
{Ref: "e3", Role: "dialog", Name: "Confirm Delete"},
{Ref: "e4", Role: "heading", Name: "Are you sure?"},
{Ref: "e5", Role: "text", Name: "This action cannot be undone. The item will be permanently deleted."},
{Ref: "e6", Role: "button", Name: "Yes, Delete"},
{Ref: "e7", Role: "button", Name: "Cancel"},
{Ref: "e8", Role: "button", Name: "Close Dialog"},
{Ref: "e9", Role: "navigation", Name: "Sidebar Menu"},
{Ref: "e10", Role: "link", Name: "Home"},
{Ref: "e11", Role: "link", Name: "Reports"},
{Ref: "e12", Role: "link", Name: "Settings"},
}
}
func TestCombinedMatcher_ComplexForm(t *testing.T) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
elements := complexFormElements()
tests := []struct {
query string
wantRef string
desc string
}{
{"submit registration", "e9", "should find the submit button"},
{"email field", "e3", "should find email textbox"},
{"terms checkbox", "e7", "should find terms of service checkbox"},
{"password input", "e4", "should find password field"},
{"cancel button", "e10", "should find cancel button"},
{"log in link", "e11", "should find the login link"},
{"country dropdown", "e6", "should find country combobox"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
result, err := m.Find(context.Background(), tt.query, elements, FindOptions{
Threshold: 0.01,
TopK: 3,
})
if err != nil {
t.Fatalf("Find error: %v", err)
}
if result.BestRef != tt.wantRef {
t.Errorf("query=%q: expected BestRef=%s, got %s (score=%f)",
tt.query, tt.wantRef, result.BestRef, result.BestScore)
for _, m := range result.Matches {
t.Logf(" match: ref=%s score=%f role=%s name=%s", m.Ref, m.Score, m.Role, m.Name)
}
}
})
}
}
func TestCombinedMatcher_ComplexTable(t *testing.T) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
elements := complexTableElements()
tests := []struct {
query string
wantRef string
desc string
}{
{"search users", "e1", "should find the search box"},
{"add new user", "e2", "should find the add button"},
{"export csv", "e3", "should find the export button"},
{"next page", "e17", "should find the next page button"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
result, err := m.Find(context.Background(), tt.query, elements, FindOptions{
Threshold: 0.01,
TopK: 3,
})
if err != nil {
t.Fatalf("Find error: %v", err)
}
if result.BestRef != tt.wantRef {
t.Errorf("query=%q: expected BestRef=%s, got %s (score=%f)",
tt.query, tt.wantRef, result.BestRef, result.BestScore)
for _, m := range result.Matches {
t.Logf(" match: ref=%s score=%f role=%s name=%s", m.Ref, m.Score, m.Role, m.Name)
}
}
})
}
}
func TestCombinedMatcher_ComplexModal(t *testing.T) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
elements := complexModalElements()
tests := []struct {
query string
wantRef string
desc string
}{
{"delete button", "e6", "should find the yes delete button in modal"},
{"close dialog", "e8", "should find the close dialog button"},
{"cancel", "e7", "should find the cancel button in modal"},
{"settings button", "e1", "should find the settings button"},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
result, err := m.Find(context.Background(), tt.query, elements, FindOptions{
Threshold: 0.01,
TopK: 3,
})
if err != nil {
t.Fatalf("Find error: %v", err)
}
if result.BestRef != tt.wantRef {
t.Errorf("query=%q: expected BestRef=%s, got %s (score=%f)",
tt.query, tt.wantRef, result.BestRef, result.BestScore)
for _, m := range result.Matches {
t.Logf(" match: ref=%s score=%f role=%s name=%s", m.Ref, m.Score, m.Role, m.Name)
}
}
})
}
}
// ===========================================================================
// Phase 3: Benchmark tests
// ===========================================================================
func BenchmarkLexicalMatcher_Find(b *testing.B) {
m := NewLexicalMatcher()
elements := complexFormElements()
opts := FindOptions{Threshold: 0.1, TopK: 3}
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = m.Find(ctx, "submit registration button", elements, opts)
}
}
func BenchmarkHashingEmbedder_Embed(b *testing.B) {
e := NewHashingEmbedder(128)
texts := []string{"submit registration button"}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Embed(texts)
}
}
func BenchmarkHashingEmbedder_EmbedBatch(b *testing.B) {
e := NewHashingEmbedder(128)
elements := complexFormElements()
texts := make([]string, len(elements))
for i, el := range elements {
texts[i] = el.Composite()
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = e.Embed(texts)
}
}
func BenchmarkEmbeddingMatcher_Find(b *testing.B) {
m := NewEmbeddingMatcher(NewHashingEmbedder(128))
elements := complexFormElements()
opts := FindOptions{Threshold: 0.1, TopK: 3}
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = m.Find(ctx, "submit registration button", elements, opts)
}
}
func BenchmarkCombinedMatcher_Find(b *testing.B) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
elements := complexFormElements()
opts := FindOptions{Threshold: 0.1, TopK: 3}
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = m.Find(ctx, "submit registration button", elements, opts)
}
}
func BenchmarkCombinedMatcher_LargeElementSet(b *testing.B) {
m := NewCombinedMatcher(NewHashingEmbedder(128))
// Build a large element set (100 elements) simulating a complex page.
elements := make([]ElementDescriptor, 100)
roles := []string{"button", "link", "textbox", "heading", "img", "checkbox", "combobox"}
for i := 0; i < 100; i++ {
elements[i] = ElementDescriptor{
Ref: fmt.Sprintf("e%d", i),
Role: roles[i%len(roles)],
Name: fmt.Sprintf("Element %d action item", i),
}
}
opts := FindOptions{Threshold: 0.1, TopK: 5}
ctx := context.Background()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = m.Find(ctx, "click the action button number 42", elements, opts)
}
}