@epsilon-asi/actors 0.0.21 → 0.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser/RuntimeConfig.d.ts +26 -0
- package/dist/browser/RuntimeConfig.d.ts.map +1 -1
- package/dist/browser/RuntimeConfig.js +29 -1
- package/dist/browser/RuntimeConfig.js.map +1 -1
- package/dist/core/ActorContext.d.ts +2 -0
- package/dist/core/ActorContext.d.ts.map +1 -1
- package/dist/core/ActorRunner.d.ts +3 -0
- package/dist/core/ActorRunner.d.ts.map +1 -1
- package/dist/core/ActorRunner.js +11 -1
- package/dist/core/ActorRunner.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/native/CompositeNativeWindowDriver.d.ts +11 -0
- package/dist/native/CompositeNativeWindowDriver.d.ts.map +1 -0
- package/dist/native/CompositeNativeWindowDriver.js +31 -0
- package/dist/native/CompositeNativeWindowDriver.js.map +1 -0
- package/dist/native/NativeActionRegistry.d.ts +14 -0
- package/dist/native/NativeActionRegistry.d.ts.map +1 -0
- package/dist/native/NativeActionRegistry.js +101 -0
- package/dist/native/NativeActionRegistry.js.map +1 -0
- package/dist/native/NativeAutomation.d.ts +3 -0
- package/dist/native/NativeAutomation.d.ts.map +1 -0
- package/dist/native/NativeAutomation.js +12 -0
- package/dist/native/NativeAutomation.js.map +1 -0
- package/dist/native/NativeCoordinateMapper.d.ts +23 -0
- package/dist/native/NativeCoordinateMapper.d.ts.map +1 -0
- package/dist/native/NativeCoordinateMapper.js +201 -0
- package/dist/native/NativeCoordinateMapper.js.map +1 -0
- package/dist/native/NativeFileDialogService.d.ts +26 -0
- package/dist/native/NativeFileDialogService.d.ts.map +1 -0
- package/dist/native/NativeFileDialogService.js +121 -0
- package/dist/native/NativeFileDialogService.js.map +1 -0
- package/dist/native/NativeImageFinder.d.ts +12 -0
- package/dist/native/NativeImageFinder.d.ts.map +1 -0
- package/dist/native/NativeImageFinder.js +29 -0
- package/dist/native/NativeImageFinder.js.map +1 -0
- package/dist/native/NativeKeyboard.d.ts +10 -0
- package/dist/native/NativeKeyboard.d.ts.map +1 -0
- package/dist/native/NativeKeyboard.js +16 -0
- package/dist/native/NativeKeyboard.js.map +1 -0
- package/dist/native/NativeMouse.d.ts +38 -0
- package/dist/native/NativeMouse.d.ts.map +1 -0
- package/dist/native/NativeMouse.js +82 -0
- package/dist/native/NativeMouse.js.map +1 -0
- package/dist/native/NativeWindowService.d.ts +31 -0
- package/dist/native/NativeWindowService.d.ts.map +1 -0
- package/dist/native/NativeWindowService.js +183 -0
- package/dist/native/NativeWindowService.js.map +1 -0
- package/dist/native/UnsupportedNativeAutomation.d.ts +4 -0
- package/dist/native/UnsupportedNativeAutomation.d.ts.map +1 -0
- package/dist/native/UnsupportedNativeAutomation.js +77 -0
- package/dist/native/UnsupportedNativeAutomation.js.map +1 -0
- package/dist/native/WindowMatcher.d.ts +4 -0
- package/dist/native/WindowMatcher.d.ts.map +1 -0
- package/dist/native/WindowMatcher.js +39 -0
- package/dist/native/WindowMatcher.js.map +1 -0
- package/dist/native/drivers.d.ts +37 -0
- package/dist/native/drivers.d.ts.map +1 -0
- package/dist/native/drivers.js +2 -0
- package/dist/native/drivers.js.map +1 -0
- package/dist/native/errors.d.ts +23 -0
- package/dist/native/errors.d.ts.map +1 -0
- package/dist/native/errors.js +45 -0
- package/dist/native/errors.js.map +1 -0
- package/dist/native/index.d.ts +13 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +13 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.d.ts +11 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.d.ts.map +1 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.js +180 -0
- package/dist/native/macos/MacOSAccessibilityWindowDriver.js.map +1 -0
- package/dist/native/macos/MacOSAppleScriptClient.d.ts +24 -0
- package/dist/native/macos/MacOSAppleScriptClient.d.ts.map +1 -0
- package/dist/native/macos/MacOSAppleScriptClient.js +163 -0
- package/dist/native/macos/MacOSAppleScriptClient.js.map +1 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.d.ts +10 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.d.ts.map +1 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.js +12 -0
- package/dist/native/macos/MacOSFileDialogAccessibilityStrategy.js.map +1 -0
- package/dist/native/macos/MacOSNativeAutomation.d.ts +3 -0
- package/dist/native/macos/MacOSNativeAutomation.d.ts.map +1 -0
- package/dist/native/macos/MacOSNativeAutomation.js +88 -0
- package/dist/native/macos/MacOSNativeAutomation.js.map +1 -0
- package/dist/native/nut/NutNativeImageFinder.d.ts +17 -0
- package/dist/native/nut/NutNativeImageFinder.d.ts.map +1 -0
- package/dist/native/nut/NutNativeImageFinder.js +84 -0
- package/dist/native/nut/NutNativeImageFinder.js.map +1 -0
- package/dist/native/nut/NutNativeKeyboardDriver.d.ts +8 -0
- package/dist/native/nut/NutNativeKeyboardDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeKeyboardDriver.js +39 -0
- package/dist/native/nut/NutNativeKeyboardDriver.js.map +1 -0
- package/dist/native/nut/NutNativeMouseDriver.d.ts +8 -0
- package/dist/native/nut/NutNativeMouseDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeMouseDriver.js +24 -0
- package/dist/native/nut/NutNativeMouseDriver.js.map +1 -0
- package/dist/native/nut/NutNativeScreenDriver.d.ts +6 -0
- package/dist/native/nut/NutNativeScreenDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeScreenDriver.js +12 -0
- package/dist/native/nut/NutNativeScreenDriver.js.map +1 -0
- package/dist/native/nut/NutNativeWindowDriver.d.ts +6 -0
- package/dist/native/nut/NutNativeWindowDriver.d.ts.map +1 -0
- package/dist/native/nut/NutNativeWindowDriver.js +53 -0
- package/dist/native/nut/NutNativeWindowDriver.js.map +1 -0
- package/dist/native/nut/loadNut.d.ts +58 -0
- package/dist/native/nut/loadNut.d.ts.map +1 -0
- package/dist/native/nut/loadNut.js +25 -0
- package/dist/native/nut/loadNut.js.map +1 -0
- package/dist/native/types.d.ts +194 -0
- package/dist/native/types.d.ts.map +1 -0
- package/dist/native/types.js +2 -0
- package/dist/native/types.js.map +1 -0
- package/dist/native/utils/appleScriptEscape.d.ts +7 -0
- package/dist/native/utils/appleScriptEscape.d.ts.map +1 -0
- package/dist/native/utils/appleScriptEscape.js +11 -0
- package/dist/native/utils/appleScriptEscape.js.map +1 -0
- package/dist/native/utils/geometry.d.ts +12 -0
- package/dist/native/utils/geometry.d.ts.map +1 -0
- package/dist/native/utils/geometry.js +77 -0
- package/dist/native/utils/geometry.js.map +1 -0
- package/dist/native/utils/redactNative.d.ts +2 -0
- package/dist/native/utils/redactNative.d.ts.map +1 -0
- package/dist/native/utils/redactNative.js +7 -0
- package/dist/native/utils/redactNative.js.map +1 -0
- package/dist/native/utils/waitFor.d.ts +7 -0
- package/dist/native/utils/waitFor.d.ts.map +1 -0
- package/dist/native/utils/waitFor.js +17 -0
- package/dist/native/utils/waitFor.js.map +1 -0
- package/dist/sites/upwork-com/upwork-com.actor.d.ts +4 -1
- package/dist/sites/upwork-com/upwork-com.actor.d.ts.map +1 -1
- package/dist/sites/upwork-com/upwork-com.actor.js +30 -12
- package/dist/sites/upwork-com/upwork-com.actor.js.map +1 -1
- package/dist/sites/upwork-com/upwork-com.types.d.ts +3 -1
- package/dist/sites/upwork-com/upwork-com.types.d.ts.map +1 -1
- package/dist/sites/upwork-com/upwork-com.types.js.map +1 -1
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.d.ts +70 -0
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.d.ts.map +1 -0
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.js +334 -0
- package/dist/sites/upwork-com/util/parseJobApplicationDetails.js.map +1 -0
- package/dist/sites/upwork-com/util/scrapeJobListing.d.ts +1 -0
- package/dist/sites/upwork-com/util/scrapeJobListing.d.ts.map +1 -1
- package/dist/sites/upwork-com/util/scrapeJobListing.js +4 -0
- package/dist/sites/upwork-com/util/scrapeJobListing.js.map +1 -1
- package/package.json +5 -1
- package/src/browser/RuntimeConfig.ts +57 -1
- package/src/core/ActorContext.ts +2 -0
- package/src/core/ActorRunner.ts +13 -1
- package/src/index.ts +2 -0
- package/src/native/CompositeNativeWindowDriver.ts +30 -0
- package/src/native/NativeActionRegistry.ts +114 -0
- package/src/native/NativeAutomation.ts +15 -0
- package/src/native/NativeCoordinateMapper.ts +258 -0
- package/src/native/NativeFileDialogService.ts +138 -0
- package/src/native/NativeImageFinder.ts +33 -0
- package/src/native/NativeKeyboard.ts +18 -0
- package/src/native/NativeMouse.ts +116 -0
- package/src/native/NativeWindowService.ts +229 -0
- package/src/native/UnsupportedNativeAutomation.ts +92 -0
- package/src/native/WindowMatcher.ts +31 -0
- package/src/native/drivers.ts +38 -0
- package/src/native/errors.ts +51 -0
- package/src/native/index.ts +12 -0
- package/src/native/macos/MacOSAccessibilityWindowDriver.ts +183 -0
- package/src/native/macos/MacOSAppleScriptClient.ts +182 -0
- package/src/native/macos/MacOSFileDialogAccessibilityStrategy.ts +11 -0
- package/src/native/macos/MacOSNativeAutomation.ts +86 -0
- package/src/native/nut/NutNativeImageFinder.ts +98 -0
- package/src/native/nut/NutNativeKeyboardDriver.ts +38 -0
- package/src/native/nut/NutNativeMouseDriver.ts +27 -0
- package/src/native/nut/NutNativeScreenDriver.ts +14 -0
- package/src/native/nut/NutNativeWindowDriver.ts +61 -0
- package/src/native/nut/loadNut.ts +86 -0
- package/src/native/types.ts +224 -0
- package/src/native/utils/appleScriptEscape.ts +11 -0
- package/src/native/utils/geometry.ts +88 -0
- package/src/native/utils/redactNative.ts +6 -0
- package/src/native/utils/waitFor.ts +25 -0
- package/src/sites/upwork-com/upwork-com.actor.ts +46 -15
- package/src/sites/upwork-com/upwork-com.types.ts +4 -1
- package/src/sites/upwork-com/util/parseJobApplicationDetails.ts +622 -0
- package/src/sites/upwork-com/util/scrapeJobListing.ts +4 -3
- package/tests/fixtures/makeContext.ts +7 -2
- package/tests/fixtures/native/FakeNativeAutomation.ts +138 -0
- package/tests/unit/browser/RuntimeConfig.native.test.ts +63 -0
- package/tests/unit/core/ActorRunner.native.test.ts +69 -0
- package/tests/unit/native/MacOSAppleScriptClient.test.ts +35 -0
- package/tests/unit/native/NativeActionRegistry.test.ts +34 -0
- package/tests/unit/native/NativeCoordinateMapper.test.ts +92 -0
- package/tests/unit/native/NativeFileDialogService.test.ts +91 -0
- package/tests/unit/native/NativeMouse.test.ts +91 -0
- package/tests/unit/native/NativeWindowService.test.ts +87 -0
- package/tests/unit/native/WindowMatcher.test.ts +32 -0
- package/tests/unit/native/appleScriptEscape.test.ts +9 -0
- package/tests/unit/sites/myvistage-com.login.test.ts +1 -1
- package/tests/unit/sites/myvistage-com.postComment.test.ts +0 -1
- package/tests/unit/sites/upwork-com.login.test.ts +1 -1
|
@@ -0,0 +1,622 @@
|
|
|
1
|
+
import type { ElementHandle, Page } from "puppeteer-core";
|
|
2
|
+
|
|
3
|
+
/* -------------------------------------------------------------------------------------------------
|
|
4
|
+
* Types
|
|
5
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
6
|
+
|
|
7
|
+
export type UpworkProposalQuestionKind =
|
|
8
|
+
| "cover_letter"
|
|
9
|
+
| "custom_question";
|
|
10
|
+
|
|
11
|
+
export type UpworkProposalControlType =
|
|
12
|
+
| "textarea"
|
|
13
|
+
| "input"
|
|
14
|
+
| "select"
|
|
15
|
+
| "unknown";
|
|
16
|
+
|
|
17
|
+
export interface UpworkProposalQuestionSelectors {
|
|
18
|
+
container: string | null;
|
|
19
|
+
label: string | null;
|
|
20
|
+
control: string | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface UpworkProposalQuestion {
|
|
24
|
+
id: string;
|
|
25
|
+
|
|
26
|
+
kind: UpworkProposalQuestionKind;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The visible prompt shown to the freelancer.
|
|
30
|
+
*
|
|
31
|
+
* Examples:
|
|
32
|
+
* - "Cover Letter"
|
|
33
|
+
* - "Describe your recent experience with similar projects"
|
|
34
|
+
*/
|
|
35
|
+
prompt: string;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* For cover letter this is always 0.
|
|
39
|
+
* For custom questions this is their order under `.questions-area`.
|
|
40
|
+
*/
|
|
41
|
+
index: number;
|
|
42
|
+
|
|
43
|
+
controlType: UpworkProposalControlType;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Best-effort CSS selectors for later filling.
|
|
47
|
+
* These are generated from stable page sections, not Vue data-v-* attrs.
|
|
48
|
+
*/
|
|
49
|
+
selectors: UpworkProposalQuestionSelectors;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `true` for Cover Letter.
|
|
53
|
+
* `true` for custom questions only when the DOM gives us a required marker/error.
|
|
54
|
+
* `null` means not determinable from the current DOM.
|
|
55
|
+
*/
|
|
56
|
+
required: boolean | null;
|
|
57
|
+
|
|
58
|
+
placeholder: string | null;
|
|
59
|
+
|
|
60
|
+
currentValue: string;
|
|
61
|
+
|
|
62
|
+
rows: number | null;
|
|
63
|
+
|
|
64
|
+
maxLength: number | null;
|
|
65
|
+
|
|
66
|
+
ariaLabelledBy: string | null;
|
|
67
|
+
|
|
68
|
+
errorMessages: string[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
export interface UpworkProposalQuestions {
|
|
73
|
+
hasAdditionalDetailsSection: boolean;
|
|
74
|
+
|
|
75
|
+
sourceUrl: string | null;
|
|
76
|
+
|
|
77
|
+
coverLetter: UpworkProposalQuestion | null;
|
|
78
|
+
|
|
79
|
+
customQuestions: UpworkProposalQuestion[];
|
|
80
|
+
|
|
81
|
+
allQuestions: UpworkProposalQuestion[];
|
|
82
|
+
|
|
83
|
+
counts: {
|
|
84
|
+
total: number;
|
|
85
|
+
custom: number;
|
|
86
|
+
hasCoverLetter: boolean;
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* -------------------------------------------------------------------------------------------------
|
|
91
|
+
* Browser-context parser
|
|
92
|
+
*
|
|
93
|
+
* IMPORTANT:
|
|
94
|
+
* This function is intentionally self-contained because Puppeteer serializes it
|
|
95
|
+
* into the browser context. Do not close over Node-side variables here.
|
|
96
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
97
|
+
|
|
98
|
+
function parseProposalQuestionsInBrowser(
|
|
99
|
+
rootNode: ParentNode | null,
|
|
100
|
+
sourceUrl: string | null,
|
|
101
|
+
): UpworkProposalQuestions {
|
|
102
|
+
const root: ParentNode = rootNode ?? document;
|
|
103
|
+
|
|
104
|
+
const cleanText = (input?: string | null): string => {
|
|
105
|
+
return (input ?? "")
|
|
106
|
+
.replace(/\u00a0/g, " ")
|
|
107
|
+
.replace(/\s+/g, " ")
|
|
108
|
+
.trim();
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const toNumber = (input?: string | null): number | null => {
|
|
112
|
+
const match = (input ?? "").match(/\d+/);
|
|
113
|
+
return match ? Number(match[0]) : null;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const slugify = (input: string): string => {
|
|
117
|
+
const slug = input
|
|
118
|
+
.toLowerCase()
|
|
119
|
+
.normalize("NFKD")
|
|
120
|
+
.replace(/[^\w\s-]/g, "")
|
|
121
|
+
.replace(/\s+/g, "-")
|
|
122
|
+
.replace(/-+/g, "-")
|
|
123
|
+
.replace(/^-|-$/g, "")
|
|
124
|
+
.slice(0, 80);
|
|
125
|
+
|
|
126
|
+
return slug || "question";
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const cssEscape = (input: string): string => {
|
|
130
|
+
const css = (globalThis as unknown as { CSS?: { escape?: (value: string) => string } }).CSS;
|
|
131
|
+
|
|
132
|
+
if (css?.escape) {
|
|
133
|
+
return css.escape(input);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return input.replace(/["\\]/g, "\\$&");
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const findCardByHeading = (
|
|
140
|
+
headingText: string,
|
|
141
|
+
): HTMLElement | null => {
|
|
142
|
+
const headings = Array.from(
|
|
143
|
+
root.querySelectorAll<HTMLElement>("h1, h2, h3, h4, h5"),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
const heading = headings.find(
|
|
147
|
+
(el) => cleanText(el.textContent).toLowerCase() === headingText.toLowerCase(),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (!heading) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return (
|
|
155
|
+
heading.closest<HTMLElement>(".fe-proposal-additional-details") ??
|
|
156
|
+
heading.closest<HTMLElement>(".additional-details") ??
|
|
157
|
+
heading.closest<HTMLElement>(".air3-card") ??
|
|
158
|
+
heading.parentElement
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const additionalDetails =
|
|
163
|
+
root.querySelector<HTMLElement>(".fe-proposal-additional-details") ??
|
|
164
|
+
root.querySelector<HTMLElement>(".additional-details") ??
|
|
165
|
+
findCardByHeading("Additional details");
|
|
166
|
+
|
|
167
|
+
const hasAdditionalDetailsSection = Boolean(additionalDetails);
|
|
168
|
+
|
|
169
|
+
const scope = additionalDetails ?? root;
|
|
170
|
+
|
|
171
|
+
const getErrorMessages = (container: Element): string[] => {
|
|
172
|
+
return Array.from(
|
|
173
|
+
container.querySelectorAll<HTMLElement>(
|
|
174
|
+
".air3-form-message-error, [role='alert'], .error, .has-error",
|
|
175
|
+
),
|
|
176
|
+
)
|
|
177
|
+
.map((el) => cleanText(el.textContent))
|
|
178
|
+
.filter(Boolean)
|
|
179
|
+
.filter((value, index, array) => array.indexOf(value) === index);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
const getControlType = (
|
|
183
|
+
control: Element | null,
|
|
184
|
+
): UpworkProposalControlType => {
|
|
185
|
+
if (!control) return "unknown";
|
|
186
|
+
|
|
187
|
+
const tag = control.tagName.toLowerCase();
|
|
188
|
+
|
|
189
|
+
if (tag === "textarea") return "textarea";
|
|
190
|
+
if (tag === "select") return "select";
|
|
191
|
+
if (tag === "input") return "input";
|
|
192
|
+
|
|
193
|
+
return "unknown";
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const getControlValue = (control: Element | null): string => {
|
|
197
|
+
if (!control) return "";
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
control instanceof HTMLTextAreaElement ||
|
|
201
|
+
control instanceof HTMLInputElement ||
|
|
202
|
+
control instanceof HTMLSelectElement
|
|
203
|
+
) {
|
|
204
|
+
return control.value ?? "";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return "";
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const getMaxLength = (control: Element | null): number | null => {
|
|
211
|
+
if (!control) return null;
|
|
212
|
+
|
|
213
|
+
const value = control.getAttribute("maxlength");
|
|
214
|
+
|
|
215
|
+
if (!value) return null;
|
|
216
|
+
|
|
217
|
+
const parsed = Number(value);
|
|
218
|
+
|
|
219
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const getRows = (control: Element | null): number | null => {
|
|
223
|
+
if (!(control instanceof HTMLTextAreaElement)) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return Number.isFinite(control.rows) ? control.rows : null;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const inferRequired = (
|
|
231
|
+
kind: UpworkProposalQuestionKind,
|
|
232
|
+
container: Element,
|
|
233
|
+
control: Element | null,
|
|
234
|
+
errors: string[],
|
|
235
|
+
): boolean | null => {
|
|
236
|
+
if (kind === "cover_letter") {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (control?.hasAttribute("required")) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const text = cleanText(container.textContent).toLowerCase();
|
|
245
|
+
const errorText = errors.join(" ").toLowerCase();
|
|
246
|
+
|
|
247
|
+
if (container.classList.contains("has-error") && /required/.test(errorText)) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (/\brequired\b/.test(errorText)) {
|
|
252
|
+
return true;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (/\brequired\b/.test(text)) {
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return null;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const buildQuestion = (
|
|
263
|
+
params: {
|
|
264
|
+
kind: UpworkProposalQuestionKind;
|
|
265
|
+
index: number;
|
|
266
|
+
container: Element;
|
|
267
|
+
baseSelector: string | null;
|
|
268
|
+
},
|
|
269
|
+
): UpworkProposalQuestion | null => {
|
|
270
|
+
const label = params.container.querySelector<HTMLElement>("label.label, label");
|
|
271
|
+
|
|
272
|
+
const control = params.container.querySelector<
|
|
273
|
+
HTMLTextAreaElement | HTMLInputElement | HTMLSelectElement
|
|
274
|
+
>(
|
|
275
|
+
[
|
|
276
|
+
"textarea",
|
|
277
|
+
"input:not([type='hidden']):not([type='file'])",
|
|
278
|
+
"select",
|
|
279
|
+
].join(", "),
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
const prompt = cleanText(label?.textContent);
|
|
283
|
+
|
|
284
|
+
if (!prompt || !control) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const controlType = getControlType(control);
|
|
289
|
+
const controlTag = control.tagName.toLowerCase();
|
|
290
|
+
const errors = getErrorMessages(params.container);
|
|
291
|
+
|
|
292
|
+
const ariaLabelledBy =
|
|
293
|
+
control.getAttribute("aria-labelledby") ||
|
|
294
|
+
control.getAttribute("aria-label") ||
|
|
295
|
+
null;
|
|
296
|
+
|
|
297
|
+
const id =
|
|
298
|
+
params.kind === "cover_letter"
|
|
299
|
+
? "cover-letter"
|
|
300
|
+
: `custom-question-${params.index + 1}-${slugify(prompt)}`;
|
|
301
|
+
|
|
302
|
+
let containerSelector: string | null = null;
|
|
303
|
+
let labelSelector: string | null = null;
|
|
304
|
+
let controlSelector: string | null = null;
|
|
305
|
+
|
|
306
|
+
if (params.kind === "cover_letter") {
|
|
307
|
+
containerSelector =
|
|
308
|
+
".fe-proposal-additional-details .cover-letter-area .form-group";
|
|
309
|
+
|
|
310
|
+
labelSelector =
|
|
311
|
+
".fe-proposal-additional-details .cover-letter-area label";
|
|
312
|
+
|
|
313
|
+
controlSelector =
|
|
314
|
+
".fe-proposal-additional-details .cover-letter-area textarea";
|
|
315
|
+
} else if (params.baseSelector) {
|
|
316
|
+
const nth = params.index + 1;
|
|
317
|
+
|
|
318
|
+
containerSelector =
|
|
319
|
+
`${params.baseSelector} .form-group:nth-of-type(${nth})`;
|
|
320
|
+
|
|
321
|
+
labelSelector =
|
|
322
|
+
`${containerSelector} label`;
|
|
323
|
+
|
|
324
|
+
controlSelector =
|
|
325
|
+
`${containerSelector} ${controlTag}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (!controlSelector && control.id) {
|
|
329
|
+
controlSelector = `#${cssEscape(control.id)}`;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return {
|
|
333
|
+
id,
|
|
334
|
+
|
|
335
|
+
kind: params.kind,
|
|
336
|
+
|
|
337
|
+
prompt,
|
|
338
|
+
|
|
339
|
+
index: params.index,
|
|
340
|
+
|
|
341
|
+
controlType,
|
|
342
|
+
|
|
343
|
+
selectors: {
|
|
344
|
+
container: containerSelector,
|
|
345
|
+
label: labelSelector,
|
|
346
|
+
control: controlSelector,
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
required: inferRequired(params.kind, params.container, control, errors),
|
|
350
|
+
|
|
351
|
+
placeholder: control.getAttribute("placeholder") || null,
|
|
352
|
+
|
|
353
|
+
currentValue: getControlValue(control),
|
|
354
|
+
|
|
355
|
+
rows: getRows(control),
|
|
356
|
+
|
|
357
|
+
maxLength: getMaxLength(control),
|
|
358
|
+
|
|
359
|
+
ariaLabelledBy,
|
|
360
|
+
|
|
361
|
+
errorMessages: errors,
|
|
362
|
+
};
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
/* -----------------------------------------------------------------------------------------------
|
|
366
|
+
* Cover Letter
|
|
367
|
+
* --------------------------------------------------------------------------------------------- */
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
const coverLetterContainer =
|
|
371
|
+
scope.querySelector<HTMLElement>(".cover-letter-area .form-group") ??
|
|
372
|
+
Array.from(scope.querySelectorAll<HTMLElement>(".form-group")).find((el) =>
|
|
373
|
+
cleanText(el.querySelector("label")?.textContent).toLowerCase() === "cover letter",
|
|
374
|
+
) ??
|
|
375
|
+
null;
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
const coverLetter = coverLetterContainer
|
|
379
|
+
? buildQuestion({
|
|
380
|
+
kind: "cover_letter",
|
|
381
|
+
index: 0,
|
|
382
|
+
container: coverLetterContainer,
|
|
383
|
+
baseSelector: ".fe-proposal-additional-details .cover-letter-area",
|
|
384
|
+
})
|
|
385
|
+
: null;
|
|
386
|
+
|
|
387
|
+
/* -----------------------------------------------------------------------------------------------
|
|
388
|
+
* Custom Job Questions
|
|
389
|
+
* --------------------------------------------------------------------------------------------- */
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
const customQuestionArea =
|
|
393
|
+
scope.querySelector<HTMLElement>(".fe-proposal-job-questions.questions-area") ??
|
|
394
|
+
scope.querySelector<HTMLElement>(".questions-area");
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
const customQuestionContainers = customQuestionArea
|
|
398
|
+
? Array.from(customQuestionArea.querySelectorAll<HTMLElement>(".form-group"))
|
|
399
|
+
.filter((el) =>
|
|
400
|
+
Boolean(
|
|
401
|
+
el.querySelector(
|
|
402
|
+
[
|
|
403
|
+
"textarea",
|
|
404
|
+
"input:not([type='hidden']):not([type='file'])",
|
|
405
|
+
"select",
|
|
406
|
+
].join(", "),
|
|
407
|
+
),
|
|
408
|
+
),
|
|
409
|
+
)
|
|
410
|
+
.filter((el) => {
|
|
411
|
+
const labelText = cleanText(el.querySelector("label")?.textContent).toLowerCase();
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
labelText !== "cover letter" &&
|
|
415
|
+
labelText !== "attachments"
|
|
416
|
+
);
|
|
417
|
+
})
|
|
418
|
+
: [];
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
const customQuestions = customQuestionContainers
|
|
422
|
+
.map((container, index) =>
|
|
423
|
+
buildQuestion({
|
|
424
|
+
kind: "custom_question",
|
|
425
|
+
index,
|
|
426
|
+
container,
|
|
427
|
+
baseSelector:
|
|
428
|
+
".fe-proposal-additional-details .fe-proposal-job-questions.questions-area",
|
|
429
|
+
}),
|
|
430
|
+
)
|
|
431
|
+
.filter((question): question is UpworkProposalQuestion => Boolean(question));
|
|
432
|
+
|
|
433
|
+
/* -----------------------------------------------------------------------------------------------
|
|
434
|
+
* Attachments metadata
|
|
435
|
+
* --------------------------------------------------------------------------------------------- */
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
const attachmentsArea =
|
|
439
|
+
scope.querySelector<HTMLElement>(".attachments-area") ??
|
|
440
|
+
Array.from(scope.querySelectorAll<HTMLElement>(".form-group")).find((el) =>
|
|
441
|
+
cleanText(el.querySelector("label")?.textContent).toLowerCase() === "attachments",
|
|
442
|
+
) ??
|
|
443
|
+
null;
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
const attachmentsText = cleanText(attachmentsArea?.textContent);
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
const fileInput =
|
|
450
|
+
attachmentsArea?.querySelector<HTMLInputElement>("input[type='file']") ??
|
|
451
|
+
null;
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
const rejectTypesHolder =
|
|
455
|
+
attachmentsArea?.querySelector<HTMLElement>("[reject_types]") ??
|
|
456
|
+
null;
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
const rejectedExtensions = cleanText(
|
|
460
|
+
rejectTypesHolder?.getAttribute("reject_types") ?? "",
|
|
461
|
+
)
|
|
462
|
+
.split(",")
|
|
463
|
+
.map((value) => value.trim().replace(/^\./, ""))
|
|
464
|
+
.filter(Boolean);
|
|
465
|
+
|
|
466
|
+
const allQuestions = [
|
|
467
|
+
...(coverLetter ? [coverLetter] : []),
|
|
468
|
+
...customQuestions,
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
hasAdditionalDetailsSection,
|
|
473
|
+
|
|
474
|
+
sourceUrl,
|
|
475
|
+
|
|
476
|
+
coverLetter,
|
|
477
|
+
|
|
478
|
+
customQuestions,
|
|
479
|
+
|
|
480
|
+
allQuestions,
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
counts: {
|
|
485
|
+
total: allQuestions.length,
|
|
486
|
+
custom: customQuestions.length,
|
|
487
|
+
hasCoverLetter: Boolean(coverLetter),
|
|
488
|
+
},
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/* -------------------------------------------------------------------------------------------------
|
|
493
|
+
* Puppeteer API
|
|
494
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
495
|
+
|
|
496
|
+
export async function parseUpworkProposalQuestions(
|
|
497
|
+
page: Page,
|
|
498
|
+
options?: {
|
|
499
|
+
timeoutMs?: number;
|
|
500
|
+
},
|
|
501
|
+
): Promise<UpworkProposalQuestions> {
|
|
502
|
+
const timeoutMs = options?.timeoutMs ?? 15_000;
|
|
503
|
+
|
|
504
|
+
await page
|
|
505
|
+
.waitForSelector(
|
|
506
|
+
[
|
|
507
|
+
".fe-proposal-additional-details",
|
|
508
|
+
".additional-details",
|
|
509
|
+
".cover-letter-area",
|
|
510
|
+
".questions-area",
|
|
511
|
+
].join(", "),
|
|
512
|
+
{
|
|
513
|
+
timeout: timeoutMs,
|
|
514
|
+
},
|
|
515
|
+
)
|
|
516
|
+
.catch(() => {
|
|
517
|
+
// Some proposal pages may render only a cover letter or may lazy-render the
|
|
518
|
+
// additional details section. We still try to parse the current DOM.
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return page.evaluate(
|
|
522
|
+
parseProposalQuestionsInBrowser,
|
|
523
|
+
null,
|
|
524
|
+
page.url(),
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
export async function parseUpworkProposalQuestionsFromRoot(
|
|
529
|
+
root: ElementHandle<Element>,
|
|
530
|
+
sourceUrl: string | null = null,
|
|
531
|
+
): Promise<UpworkProposalQuestions> {
|
|
532
|
+
return root.evaluate(
|
|
533
|
+
parseProposalQuestionsInBrowser,
|
|
534
|
+
sourceUrl,
|
|
535
|
+
);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* -------------------------------------------------------------------------------------------------
|
|
539
|
+
* Optional helper: fill answers after parsing
|
|
540
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
541
|
+
|
|
542
|
+
export interface UpworkProposalQuestionAnswers {
|
|
543
|
+
coverLetter?: string;
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* You can key custom answers by:
|
|
547
|
+
* - generated question id
|
|
548
|
+
* - exact prompt text
|
|
549
|
+
*/
|
|
550
|
+
customQuestions?: Record<string, string>;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
export async function fillUpworkProposalQuestions(
|
|
554
|
+
page: Page,
|
|
555
|
+
answers: UpworkProposalQuestionAnswers,
|
|
556
|
+
): Promise<void> {
|
|
557
|
+
const parsed = await parseUpworkProposalQuestions(page);
|
|
558
|
+
|
|
559
|
+
if (answers.coverLetter && parsed.coverLetter?.selectors.control) {
|
|
560
|
+
await page.focus(parsed.coverLetter.selectors.control);
|
|
561
|
+
await page.keyboard.down(process.platform === "darwin" ? "Meta" : "Control");
|
|
562
|
+
await page.keyboard.press("A");
|
|
563
|
+
await page.keyboard.up(process.platform === "darwin" ? "Meta" : "Control");
|
|
564
|
+
await page.type(parsed.coverLetter.selectors.control, answers.coverLetter);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
const customAnswers = answers.customQuestions ?? {};
|
|
568
|
+
|
|
569
|
+
for (const question of parsed.customQuestions) {
|
|
570
|
+
const answer =
|
|
571
|
+
customAnswers[question.id] ??
|
|
572
|
+
customAnswers[question.prompt];
|
|
573
|
+
|
|
574
|
+
if (!answer || !question.selectors.control) {
|
|
575
|
+
continue;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
await page.focus(question.selectors.control);
|
|
579
|
+
await page.keyboard.down(process.platform === "darwin" ? "Meta" : "Control");
|
|
580
|
+
await page.keyboard.press("A");
|
|
581
|
+
await page.keyboard.up(process.platform === "darwin" ? "Meta" : "Control");
|
|
582
|
+
await page.type(question.selectors.control, answer);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/* -------------------------------------------------------------------------------------------------
|
|
587
|
+
* Example
|
|
588
|
+
* ------------------------------------------------------------------------------------------------- */
|
|
589
|
+
|
|
590
|
+
/*
|
|
591
|
+
import puppeteer from "puppeteer";
|
|
592
|
+
|
|
593
|
+
async function main(): Promise<void> {
|
|
594
|
+
const browser = await puppeteer.launch({
|
|
595
|
+
headless: false,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
const page = await browser.newPage();
|
|
599
|
+
|
|
600
|
+
await page.goto("https://www.upwork.com/ab/proposals/job/~YOUR_JOB_ID/apply/", {
|
|
601
|
+
waitUntil: "networkidle2",
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
const questions = await parseUpworkProposalQuestions(page);
|
|
605
|
+
|
|
606
|
+
console.dir(questions, {
|
|
607
|
+
depth: null,
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
await fillUpworkProposalQuestions(page, {
|
|
611
|
+
coverLetter: "Hi, I have relevant experience...",
|
|
612
|
+
customQuestions: {
|
|
613
|
+
"Describe your recent experience with similar projects":
|
|
614
|
+
"Recently I led a similar product and engineering effort...",
|
|
615
|
+
},
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
await browser.close();
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
void main();
|
|
622
|
+
*/
|
|
@@ -14,7 +14,7 @@ export const UpworkJobListingSchema = z.object({
|
|
|
14
14
|
|
|
15
15
|
title: z.string(),
|
|
16
16
|
url: z.string(),
|
|
17
|
-
|
|
17
|
+
alreadyApplied: z.boolean(),
|
|
18
18
|
postedAtText: z.string().nullable(),
|
|
19
19
|
proposalsText: z.string().nullable(),
|
|
20
20
|
|
|
@@ -155,6 +155,7 @@ export async function parseUpworkJobListing(
|
|
|
155
155
|
node.querySelectorAll('[data-test="JobInfoClient"] li'),
|
|
156
156
|
).map((li) => li.textContent?.trim() ?? "");
|
|
157
157
|
|
|
158
|
+
const alreadyApplied = !!node.querySelector('[data-test="JobBadgeApplied"]');
|
|
158
159
|
const hourlyItem =
|
|
159
160
|
jobInfoItems.find((x) => x.toLowerCase().includes("hourly")) ?? null;
|
|
160
161
|
|
|
@@ -191,7 +192,7 @@ export async function parseUpworkJobListing(
|
|
|
191
192
|
node.getAttribute("data-ev-job-uid") ??
|
|
192
193
|
node.getAttribute("data-test-key") ??
|
|
193
194
|
"",
|
|
194
|
-
|
|
195
|
+
alreadyApplied,
|
|
195
196
|
title: titleAnchor?.textContent?.trim() ?? "",
|
|
196
197
|
|
|
197
198
|
url: titleAnchor?.href ?? "",
|
|
@@ -237,7 +238,7 @@ export async function parseUpworkJobListing(
|
|
|
237
238
|
|
|
238
239
|
const parsed: UpworkJobListing = {
|
|
239
240
|
id: cleanText(extracted.id),
|
|
240
|
-
|
|
241
|
+
alreadyApplied: extracted.alreadyApplied,
|
|
241
242
|
title: cleanText(extracted.title),
|
|
242
243
|
|
|
243
244
|
url: cleanText(extracted.url),
|
|
@@ -13,6 +13,7 @@ import { PuppeteerPageAdapter } from '../../src/interaction/PageAdapter.js';
|
|
|
13
13
|
import { MemoryLogger } from '../../src/logging/MemoryLogger.js';
|
|
14
14
|
import { FakeHumanInteractor } from './FakeCursor.js';
|
|
15
15
|
import { FakeBrowser, FakePage } from './FakePage.js';
|
|
16
|
+
import { createFakeNativeAutomation } from './native/FakeNativeAutomation.js';
|
|
16
17
|
|
|
17
18
|
export interface TestContextParts {
|
|
18
19
|
context: ActorContext;
|
|
@@ -20,6 +21,7 @@ export interface TestContextParts {
|
|
|
20
21
|
fakeInteractor: FakeHumanInteractor;
|
|
21
22
|
logger: MemoryLogger;
|
|
22
23
|
config: RuntimeConfig;
|
|
24
|
+
native: ReturnType<typeof createFakeNativeAutomation>;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
export function makeContext(overrides: Partial<{
|
|
@@ -28,6 +30,7 @@ export function makeContext(overrides: Partial<{
|
|
|
28
30
|
actorId: string;
|
|
29
31
|
baseUrl: string;
|
|
30
32
|
auth: AuthController;
|
|
33
|
+
native: ReturnType<typeof createFakeNativeAutomation>;
|
|
31
34
|
}> = {}): TestContextParts {
|
|
32
35
|
const fakePage = overrides.page ?? new FakePage();
|
|
33
36
|
const fakeInteractor = overrides.interactor instanceof FakeHumanInteractor
|
|
@@ -52,6 +55,7 @@ export function makeContext(overrides: Partial<{
|
|
|
52
55
|
const extract = new Extractor(page);
|
|
53
56
|
const pagination = new Pagination(page, cursor);
|
|
54
57
|
const logger = new MemoryLogger();
|
|
58
|
+
const native = overrides.native ?? createFakeNativeAutomation(logger);
|
|
55
59
|
|
|
56
60
|
const auth: AuthController = overrides.auth ?? {
|
|
57
61
|
isLoggedIn: async () => false,
|
|
@@ -69,8 +73,9 @@ export function makeContext(overrides: Partial<{
|
|
|
69
73
|
extract,
|
|
70
74
|
pagination,
|
|
71
75
|
auth,
|
|
72
|
-
logger
|
|
76
|
+
logger,
|
|
77
|
+
native
|
|
73
78
|
};
|
|
74
79
|
|
|
75
|
-
return { context, fakePage, fakeInteractor, logger, config };
|
|
80
|
+
return { context, fakePage, fakeInteractor, logger, config, native };
|
|
76
81
|
}
|