File size: 4,186 Bytes
6a7089a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package semantic

import "strings"

// FailureType classifies the cause of an action failure so the recovery
// engine can decide whether and how to attempt self-healing.
type FailureType int

const (
	// FailureUnknown β€” the error could not be classified; not recoverable.
	FailureUnknown FailureType = iota

	// FailureElementNotFound β€” the ref no longer maps to a DOM node.
	// Recoverable via semantic re-match on a fresh snapshot.
	FailureElementNotFound

	// FailureElementStale β€” the node exists but has been detached from
	// the live DOM (SPA re-render, modal overlay, etc.).
	FailureElementStale

	// FailureElementNotInteractable β€” the element is hidden, disabled,
	// or covered by an overlay and cannot receive input.
	FailureElementNotInteractable

	// FailureNavigation β€” the action triggered a page navigation; the
	// entire snapshot is invalid. Recoverable via fresh snapshot.
	FailureNavigation

	// FailureNetwork β€” a transport or Chrome-level error; not recoverable
	// by the recovery engine.
	FailureNetwork
)

// String returns a human-readable label for the failure type.
func (f FailureType) String() string {
	switch f {
	case FailureElementNotFound:
		return "element_not_found"
	case FailureElementStale:
		return "element_stale"
	case FailureElementNotInteractable:
		return "element_not_interactable"
	case FailureNavigation:
		return "navigation"
	case FailureNetwork:
		return "network"
	default:
		return "unknown"
	}
}

// Recoverable reports whether the failure type is eligible for automatic
// self-healing. Network and unknown failures are not recoverable.
func (f FailureType) Recoverable() bool {
	switch f {
	case FailureElementNotFound,
		FailureElementStale,
		FailureElementNotInteractable,
		FailureNavigation:
		return true
	default:
		return false
	}
}

// ClassifyFailure inspects an error string and returns the most likely
// FailureType. The classification is intentionally broad β€” it matches
// error messages produced by Chrome DevTools Protocol, chromedp, and
// PinchTab's own bridge layer so that recovery works regardless of which
// layer reported the failure.
func ClassifyFailure(err error) FailureType {
	if err == nil {
		return FailureUnknown
	}
	e := strings.ToLower(err.Error())

	// Element not found patterns.
	notFoundPatterns := []string{
		"could not find node",
		"node with given id",
		"no node",
		"ref not found",
		"node not found",
		"backend node id",
		"no node with given",
	}
	for _, p := range notFoundPatterns {
		if strings.Contains(e, p) {
			return FailureElementNotFound
		}
	}

	// Stale element / detached DOM patterns.
	// NOTE: "frame" detachment is classified as navigation, not stale.
	stalePatterns := []string{
		"stale",
		"orphan",
		"object reference",
		"node is detached",
		"execution context was destroyed",
		"context was destroyed",
		"target closed",
	}
	for _, p := range stalePatterns {
		if strings.Contains(e, p) {
			return FailureElementStale
		}
	}
	// "detached" alone is stale, but "frame" + "detached" is navigation.
	if strings.Contains(e, "detached") && !strings.Contains(e, "frame") {
		return FailureElementStale
	}

	// Not interactable patterns.
	interactablePatterns := []string{
		"not interactable",
		"not clickable",
		"element is not visible",
		"not visible",
		"element is disabled",
		"overlapped",
		"overlapping",
		"obscured",
		"pointer-events: none",
		"cannot focus",
		"outside of the viewport",
		"outside the viewport",
	}
	for _, p := range interactablePatterns {
		if strings.Contains(e, p) {
			return FailureElementNotInteractable
		}
	}

	// Navigation patterns.
	navPatterns := []string{
		"navigat",
		"page crashed",
		"page was destroyed",
		"inspected target",
		"frame was detached",
		"frame detached",
	}
	for _, p := range navPatterns {
		if strings.Contains(e, p) {
			return FailureNavigation
		}
	}

	// Network patterns.
	networkPatterns := []string{
		"net::",
		"connection refused",
		"could not connect",
		"timeout",
		"deadline exceeded",
		"websocket",
		"eof",
		"broken pipe",
	}
	for _, p := range networkPatterns {
		if strings.Contains(e, p) {
			return FailureNetwork
		}
	}

	return FailureUnknown
}