@milldr/crono 0.4.0 → 0.6.1
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/README.md +71 -13
- package/dist/automation/client.d.ts +5 -0
- package/dist/automation/client.d.ts.map +1 -0
- package/dist/automation/client.js +13 -0
- package/dist/automation/client.js.map +1 -0
- package/dist/automation/runner.d.ts +3 -0
- package/dist/automation/runner.d.ts.map +1 -0
- package/dist/automation/runner.js +137 -0
- package/dist/automation/runner.js.map +1 -0
- package/dist/automation/types.d.ts +66 -0
- package/dist/automation/types.d.ts.map +1 -0
- package/dist/automation/types.js +2 -0
- package/dist/automation/types.js.map +1 -0
- package/dist/commands/add.js +3 -3
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/diary.d.ts +1 -1
- package/dist/commands/diary.d.ts.map +1 -1
- package/dist/commands/diary.js +3 -3
- package/dist/commands/diary.js.map +1 -1
- package/dist/commands/export.d.ts +1 -0
- package/dist/commands/export.d.ts.map +1 -1
- package/dist/commands/export.js +45 -3
- package/dist/commands/export.js.map +1 -1
- package/dist/commands/log.js +3 -3
- package/dist/commands/log.js.map +1 -1
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +53 -26
- package/dist/commands/login.js.map +1 -1
- package/dist/commands/quick-add.js +3 -3
- package/dist/commands/quick-add.js.map +1 -1
- package/dist/commands/recipes.d.ts +5 -0
- package/dist/commands/recipes.d.ts.map +1 -0
- package/dist/commands/recipes.js +35 -0
- package/dist/commands/recipes.js.map +1 -0
- package/dist/commands/weight.d.ts.map +1 -1
- package/dist/commands/weight.js +3 -3
- package/dist/commands/weight.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -0
- package/dist/config.js.map +1 -1
- package/dist/cronometer/export.d.ts +1 -1
- package/dist/cronometer/export.d.ts.map +1 -1
- package/dist/cronometer/export.js +1 -0
- package/dist/cronometer/export.js.map +1 -1
- package/dist/cronometer/parse.d.ts +14 -0
- package/dist/cronometer/parse.d.ts.map +1 -1
- package/dist/cronometer/parse.js +62 -0
- package/dist/cronometer/parse.js.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/kernel/add-custom-food.d.ts.map +1 -1
- package/dist/kernel/add-custom-food.js +6 -127
- package/dist/kernel/add-custom-food.js.map +1 -1
- package/dist/kernel/client.d.ts +4 -56
- package/dist/kernel/client.d.ts.map +1 -1
- package/dist/kernel/client.js +29 -274
- package/dist/kernel/client.js.map +1 -1
- package/dist/kernel/food-dialog.d.ts +13 -0
- package/dist/kernel/food-dialog.d.ts.map +1 -0
- package/dist/kernel/food-dialog.js +294 -0
- package/dist/kernel/food-dialog.js.map +1 -0
- package/dist/kernel/log-food.d.ts.map +1 -1
- package/dist/kernel/log-food.js +2 -175
- package/dist/kernel/log-food.js.map +1 -1
- package/dist/kernel/login.d.ts +1 -1
- package/dist/kernel/login.d.ts.map +1 -1
- package/dist/kernel/login.js +34 -4
- package/dist/kernel/login.js.map +1 -1
- package/dist/kernel/quick-add.d.ts.map +1 -1
- package/dist/kernel/quick-add.js +9 -186
- package/dist/kernel/quick-add.js.map +1 -1
- package/dist/kernel/recipes.d.ts +10 -0
- package/dist/kernel/recipes.d.ts.map +1 -0
- package/dist/kernel/recipes.js +38 -0
- package/dist/kernel/recipes.js.map +1 -0
- package/dist/playwright/client.d.ts +6 -0
- package/dist/playwright/client.d.ts.map +1 -0
- package/dist/playwright/client.js +41 -0
- package/dist/playwright/client.js.map +1 -0
- package/package.json +3 -2
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
export function buildFoodDialogCode(options = {}) {
|
|
2
|
+
const foodNameVar = options.foodNameVar ?? "foodName";
|
|
3
|
+
const mealLabelVar = options.mealLabelVar ?? "mealLabel";
|
|
4
|
+
const errorPrefix = options.errorPrefix ?? "";
|
|
5
|
+
const itemNameVar = options.itemNameVar ?? foodNameVar;
|
|
6
|
+
const requireServingSize = options.requireServingSize ?? true;
|
|
7
|
+
const servingCountVar = options.servingCountVar ?? "servingCount";
|
|
8
|
+
const alwaysUpdateServingSize = options.alwaysUpdateServingSize ?? false;
|
|
9
|
+
const updateServingSize = options.updateServingSize ?? false;
|
|
10
|
+
const verifyDialogDismissed = options.verifyDialogDismissed ?? false;
|
|
11
|
+
const mealCategoryError = errorPrefix
|
|
12
|
+
? `${errorPrefix}could not find meal category "`
|
|
13
|
+
: 'Could not find meal category "';
|
|
14
|
+
const contextMenuError = errorPrefix
|
|
15
|
+
? `${errorPrefix}context menu did not appear after right-clicking "`
|
|
16
|
+
: 'Context menu did not appear after right-clicking "';
|
|
17
|
+
const addFoodError = errorPrefix
|
|
18
|
+
? `${errorPrefix}could not find "Add Food" in context menu`
|
|
19
|
+
: 'Could not find "Add Food" in context menu';
|
|
20
|
+
const searchBarError = errorPrefix
|
|
21
|
+
? `${errorPrefix}could not find food search bar in Add Food dialog`
|
|
22
|
+
: "Could not find food search bar in Add Food dialog";
|
|
23
|
+
const noFoodError = errorPrefix
|
|
24
|
+
? `${errorPrefix}no food found matching "`
|
|
25
|
+
: 'No food found matching "';
|
|
26
|
+
const noExactResultError = errorPrefix
|
|
27
|
+
? `${errorPrefix}no exact search result found for "`
|
|
28
|
+
: 'No exact search result found for "';
|
|
29
|
+
const addToDiaryError = errorPrefix
|
|
30
|
+
? `${errorPrefix}could not find "Add to Diary" button`
|
|
31
|
+
: 'Could not find "Add to Diary" button';
|
|
32
|
+
const addToDiaryErrorCode = itemNameVar === foodNameVar
|
|
33
|
+
? `'${addToDiaryError}'`
|
|
34
|
+
: `'${addToDiaryError} for "' + ${itemNameVar} + '"'`;
|
|
35
|
+
return `
|
|
36
|
+
// Helper: right-click an element from a list of selectors
|
|
37
|
+
async function rightClickFirst(selectors, description) {
|
|
38
|
+
for (const sel of selectors) {
|
|
39
|
+
try {
|
|
40
|
+
const el = page.locator(sel);
|
|
41
|
+
if (await el.count() > 0) {
|
|
42
|
+
await el.first().click({ button: 'right', timeout: 5000 });
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Right-click meal category with retry (GWT context menus can be flaky
|
|
51
|
+
// and meal labels may not render immediately)
|
|
52
|
+
let menuVisible = false;
|
|
53
|
+
for (let attempt = 0; attempt < 3 && !menuVisible; attempt++) {
|
|
54
|
+
if (attempt > 0) {
|
|
55
|
+
await page.keyboard.press('Escape');
|
|
56
|
+
await page.mouse.click(1, 1);
|
|
57
|
+
await page.waitForTimeout(2000);
|
|
58
|
+
}
|
|
59
|
+
const clicked = await rightClickFirst([
|
|
60
|
+
'td:text("' + ${mealLabelVar} + '")',
|
|
61
|
+
'div:text("' + ${mealLabelVar} + '")',
|
|
62
|
+
'text="' + ${mealLabelVar} + '"',
|
|
63
|
+
], 'meal category');
|
|
64
|
+
if (!clicked) {
|
|
65
|
+
if (attempt < 2) {
|
|
66
|
+
await page.waitForTimeout(2000);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
return { success: false, error: '${mealCategoryError}' + ${mealLabelVar} + '" in diary' };
|
|
70
|
+
}
|
|
71
|
+
menuVisible = await page.waitForSelector('text="Add Food..."', { timeout: 3000 })
|
|
72
|
+
.then(() => true)
|
|
73
|
+
.catch(() => page.waitForSelector('text="Add Food"', { timeout: 2000 }).then(() => true).catch(() => false));
|
|
74
|
+
}
|
|
75
|
+
if (!menuVisible) {
|
|
76
|
+
return { success: false, error: '${contextMenuError}' + ${mealLabelVar} + '"' };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Click "Add Food..." in context menu
|
|
80
|
+
const addFoodClicked = await clickFirst([
|
|
81
|
+
'text="Add Food..."',
|
|
82
|
+
'text="Add Food\\u2026"',
|
|
83
|
+
'text="Add Food"',
|
|
84
|
+
'[role="menuitem"]:has-text("Add Food")',
|
|
85
|
+
], 'Add Food menu item');
|
|
86
|
+
if (!addFoodClicked) {
|
|
87
|
+
return { success: false, error: '${addFoodError}' };
|
|
88
|
+
}
|
|
89
|
+
await page.waitForTimeout(200);
|
|
90
|
+
|
|
91
|
+
// Wait for "Add Food to Diary" dialog
|
|
92
|
+
try {
|
|
93
|
+
await page.waitForSelector('text="Add Food to Diary"', { timeout: 5000 });
|
|
94
|
+
} catch {
|
|
95
|
+
return { success: false, error: '${errorPrefix}Add Food to Diary dialog did not appear' };
|
|
96
|
+
}
|
|
97
|
+
await page.waitForTimeout(300);
|
|
98
|
+
|
|
99
|
+
// Search for the food
|
|
100
|
+
const searchSelectors = [
|
|
101
|
+
'input[placeholder*="Search all foods" i]',
|
|
102
|
+
'input[placeholder*="Search" i]',
|
|
103
|
+
'input[placeholder*="food" i]',
|
|
104
|
+
'input.gwt-TextBox',
|
|
105
|
+
'input[type="text"]',
|
|
106
|
+
'input[type="search"]',
|
|
107
|
+
];
|
|
108
|
+
let searched = false;
|
|
109
|
+
for (const sel of searchSelectors) {
|
|
110
|
+
try {
|
|
111
|
+
const el = page.locator(sel);
|
|
112
|
+
if (await el.count() > 0) {
|
|
113
|
+
await el.first().click();
|
|
114
|
+
await page.waitForTimeout(200);
|
|
115
|
+
await el.first().fill('');
|
|
116
|
+
await page.keyboard.type(${foodNameVar}, { delay: 50 });
|
|
117
|
+
searched = true;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
} catch {}
|
|
121
|
+
}
|
|
122
|
+
if (!searched) {
|
|
123
|
+
return { success: false, error: '${searchBarError}' };
|
|
124
|
+
}
|
|
125
|
+
await page.waitForTimeout(300);
|
|
126
|
+
|
|
127
|
+
// Click SEARCH
|
|
128
|
+
await clickFirst([
|
|
129
|
+
'text="SEARCH")',
|
|
130
|
+
'button:has-text("SEARCH")',
|
|
131
|
+
'button:has-text("Search")',
|
|
132
|
+
], 'SEARCH button');
|
|
133
|
+
|
|
134
|
+
// Wait for search results to appear
|
|
135
|
+
const resultsAppeared = await page.waitForSelector('td', { timeout: 8000 })
|
|
136
|
+
.then(() => true)
|
|
137
|
+
.catch(() => false);
|
|
138
|
+
if (!resultsAppeared) {
|
|
139
|
+
return { success: false, error: '${noFoodError}' + ${foodNameVar} + '"' };
|
|
140
|
+
}
|
|
141
|
+
await page.waitForTimeout(2000);
|
|
142
|
+
|
|
143
|
+
// Select the first visible result row whose Description cell exactly
|
|
144
|
+
// matches the requested food. Cronometer renders duplicate result rows, so
|
|
145
|
+
// iterate them and click the first good row. Only consider rows with exactly
|
|
146
|
+
// 2 cells (Description + Source columns in search results), not diary entry
|
|
147
|
+
// rows (8+ cells). We scan ALL <tr> elements because the search results table
|
|
148
|
+
// is distinct from the diary table shown in the background.
|
|
149
|
+
const allRows = page.locator('tr');
|
|
150
|
+
let resultClicked = false;
|
|
151
|
+
const allRowCount = await allRows.count();
|
|
152
|
+
console.log('[crono food-dialog] scanning ' + allRowCount + ' total rows');
|
|
153
|
+
|
|
154
|
+
for (let i = 0; i < allRowCount; i++) {
|
|
155
|
+
const row = allRows.nth(i);
|
|
156
|
+
const visible = await row.isVisible().catch(() => false);
|
|
157
|
+
if (!visible) continue;
|
|
158
|
+
|
|
159
|
+
// Only consider rows with exactly 2 cells (search results table)
|
|
160
|
+
const cellCount = await row.locator('td').count();
|
|
161
|
+
if (cellCount !== 2) continue;
|
|
162
|
+
|
|
163
|
+
const description = (await row.locator('td').first().textContent().catch(() => '') || '').trim();
|
|
164
|
+
if (description !== ${foodNameVar}) continue;
|
|
165
|
+
|
|
166
|
+
console.log('[crono food-dialog] found matching search result at row ' + i + ': ' + description);
|
|
167
|
+
// GWT rows don't respond to click(), need to focus and press Enter
|
|
168
|
+
await row.focus();
|
|
169
|
+
await page.waitForTimeout(300);
|
|
170
|
+
await page.keyboard.press('Enter');
|
|
171
|
+
resultClicked = true;
|
|
172
|
+
break;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!resultClicked) {
|
|
176
|
+
return { success: false, error: '${noExactResultError}' + ${foodNameVar} + '"' };
|
|
177
|
+
}
|
|
178
|
+
await page.waitForTimeout(1000);
|
|
179
|
+
|
|
180
|
+
const dialogStillOpen = await page.waitForSelector('text="Add Food to Diary"', { timeout: 1000 })
|
|
181
|
+
.then(() => true)
|
|
182
|
+
.catch(() => false);
|
|
183
|
+
if (!dialogStillOpen) {
|
|
184
|
+
return { success: false, error: 'Add Food dialog closed before details loaded after selecting "' + ${foodNameVar} + '"' };
|
|
185
|
+
}
|
|
186
|
+
console.log('[crono food-dialog] selected search result and dialog stayed open: ' + ${foodNameVar});
|
|
187
|
+
|
|
188
|
+
${buildServingSizeCode({ errorPrefix, foodNameVar, itemNameVar, requireServingSize, servingCountVar, alwaysUpdateServingSize, updateServingSize })}
|
|
189
|
+
|
|
190
|
+
// Click "Add to Diary" button - wait for panel to render, then find button using evaluate
|
|
191
|
+
await page.waitForTimeout(2000);
|
|
192
|
+
|
|
193
|
+
// Find and click the button using page.evaluate
|
|
194
|
+
const buttonClicked = await page.evaluate(() => {
|
|
195
|
+
const buttons = document.querySelectorAll('button');
|
|
196
|
+
for (const btn of buttons) {
|
|
197
|
+
if (btn.textContent && btn.textContent.trim() === 'Add to Diary' && btn.offsetParent !== null) {
|
|
198
|
+
btn.click();
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!buttonClicked) {
|
|
206
|
+
return { success: false, error: ${addToDiaryErrorCode} };
|
|
207
|
+
}
|
|
208
|
+
console.log('[crono food-dialog] found and clicked Add to Diary button for: ' + ${itemNameVar});
|
|
209
|
+
console.log('[crono food-dialog] clicked Add to Diary for: ' + ${itemNameVar});
|
|
210
|
+
${buildDialogDismissedCode({ itemNameVar, verifyDialogDismissed })}
|
|
211
|
+
`;
|
|
212
|
+
}
|
|
213
|
+
function buildServingSizeCode(options) {
|
|
214
|
+
const servingCondition = options.alwaysUpdateServingSize
|
|
215
|
+
? "true"
|
|
216
|
+
: `${options.servingCountVar} !== 1`;
|
|
217
|
+
const requireBlock = options.requireServingSize
|
|
218
|
+
? `
|
|
219
|
+
// Wait for the detail panel with serving size
|
|
220
|
+
try {
|
|
221
|
+
await page.waitForSelector('text="Serving Size"', { timeout: 5000 });
|
|
222
|
+
console.log('[crono food-dialog] found Serving Size panel for: ' + ${options.itemNameVar});
|
|
223
|
+
} catch {
|
|
224
|
+
return { success: false, error: '${options.errorPrefix}Serving Size panel did not appear for "' + ${options.itemNameVar} + '"' };
|
|
225
|
+
}
|
|
226
|
+
await page.waitForTimeout(500);
|
|
227
|
+
`
|
|
228
|
+
: "";
|
|
229
|
+
if (!options.updateServingSize) {
|
|
230
|
+
return requireBlock;
|
|
231
|
+
}
|
|
232
|
+
return `${requireBlock}
|
|
233
|
+
// If needed, update the serving size input
|
|
234
|
+
if (${servingCondition}) {
|
|
235
|
+
let servingFilled = false;
|
|
236
|
+
try {
|
|
237
|
+
servingFilled = await page.evaluate((count) => {
|
|
238
|
+
const walker = document.createTreeWalker(
|
|
239
|
+
document.body,
|
|
240
|
+
NodeFilter.SHOW_TEXT,
|
|
241
|
+
{ acceptNode: (node) =>
|
|
242
|
+
node.textContent && node.textContent.trim() === 'Serving Size'
|
|
243
|
+
? NodeFilter.FILTER_ACCEPT
|
|
244
|
+
: NodeFilter.FILTER_REJECT
|
|
245
|
+
}
|
|
246
|
+
);
|
|
247
|
+
const textNode = walker.nextNode();
|
|
248
|
+
if (!textNode) return false;
|
|
249
|
+
|
|
250
|
+
let container = textNode.parentElement;
|
|
251
|
+
for (let i = 0; i < 5 && container; i++) {
|
|
252
|
+
const input = container.querySelector('input');
|
|
253
|
+
if (input) {
|
|
254
|
+
input.focus();
|
|
255
|
+
input.select();
|
|
256
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
257
|
+
window.HTMLInputElement.prototype, 'value'
|
|
258
|
+
).set;
|
|
259
|
+
nativeSetter.call(input, String(count));
|
|
260
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
261
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
container = container.parentElement;
|
|
265
|
+
}
|
|
266
|
+
return false;
|
|
267
|
+
}, ${options.servingCountVar});
|
|
268
|
+
} catch {}
|
|
269
|
+
|
|
270
|
+
if (!servingFilled) {
|
|
271
|
+
return { success: false, error: '${options.errorPrefix}could not update serving size for "' + ${options.itemNameVar} + '"' };
|
|
272
|
+
}
|
|
273
|
+
console.log('[crono food-dialog] updated serving size for: ' + ${options.itemNameVar});
|
|
274
|
+
await page.waitForTimeout(500);
|
|
275
|
+
}
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
function buildDialogDismissedCode(options) {
|
|
279
|
+
if (!options.verifyDialogDismissed) {
|
|
280
|
+
return `
|
|
281
|
+
await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 }).catch(() => {});
|
|
282
|
+
await page.waitForTimeout(300);
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
return `
|
|
286
|
+
const dialogDismissed = await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 })
|
|
287
|
+
.then(() => true).catch(() => false);
|
|
288
|
+
if (!dialogDismissed) {
|
|
289
|
+
return { success: false, error: '"Add Food to Diary" dialog did not close after adding "' + ${options.itemNameVar} + '"' };
|
|
290
|
+
}
|
|
291
|
+
await page.waitForTimeout(500);
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
//# sourceMappingURL=food-dialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"food-dialog.js","sourceRoot":"","sources":["../../src/kernel/food-dialog.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-food.d.ts","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"log-food.d.ts","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGhD;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,CA2C5D"}
|
package/dist/kernel/log-food.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* kernel.browsers.playwright.execute(). The code has access to
|
|
6
6
|
* `page`, `context`, and `browser` from the Playwright environment.
|
|
7
7
|
*/
|
|
8
|
+
import { buildFoodDialogCode } from "./food-dialog.js";
|
|
8
9
|
/**
|
|
9
10
|
* Generate Playwright code for logging a food to the Cronometer diary.
|
|
10
11
|
*
|
|
@@ -48,181 +49,7 @@ export function buildLogFoodCode(entry) {
|
|
|
48
49
|
return false;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
async function rightClickFirst(selectors, description) {
|
|
53
|
-
for (const sel of selectors) {
|
|
54
|
-
try {
|
|
55
|
-
const el = page.locator(sel);
|
|
56
|
-
if (await el.count() > 0) {
|
|
57
|
-
await el.first().click({ button: 'right' });
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
} catch {}
|
|
61
|
-
}
|
|
62
|
-
return false;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Right-click the meal category
|
|
66
|
-
const clicked = await rightClickFirst([
|
|
67
|
-
'text="' + mealLabel + '"',
|
|
68
|
-
':has-text("' + mealLabel + '")',
|
|
69
|
-
], 'meal category');
|
|
70
|
-
if (!clicked) {
|
|
71
|
-
return { success: false, error: 'Could not find meal category "' + mealLabel + '" in diary' };
|
|
72
|
-
}
|
|
73
|
-
await page.waitForSelector('text="Add Food..."', { timeout: 3000 }).catch(() =>
|
|
74
|
-
page.waitForSelector('text="Add Food"', { timeout: 2000 }).catch(() => {})
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
// Click "Add Food..." in context menu
|
|
78
|
-
const addFoodClicked = await clickFirst([
|
|
79
|
-
'text="Add Food..."',
|
|
80
|
-
'text="Add Food\u2026"',
|
|
81
|
-
'text="Add Food"',
|
|
82
|
-
'[role="menuitem"]:has-text("Add Food")',
|
|
83
|
-
], 'Add Food menu item');
|
|
84
|
-
if (!addFoodClicked) {
|
|
85
|
-
return { success: false, error: 'Could not find "Add Food" in context menu' };
|
|
86
|
-
}
|
|
87
|
-
await page.waitForTimeout(200);
|
|
88
|
-
|
|
89
|
-
// Wait for "Add Food to Diary" dialog
|
|
90
|
-
try {
|
|
91
|
-
await page.waitForSelector('text="Add Food to Diary"', { timeout: 5000 });
|
|
92
|
-
} catch {
|
|
93
|
-
return { success: false, error: 'Add Food to Diary dialog did not appear' };
|
|
94
|
-
}
|
|
95
|
-
await page.waitForTimeout(300);
|
|
96
|
-
|
|
97
|
-
// Search for the food
|
|
98
|
-
const searchSelectors = [
|
|
99
|
-
'input[placeholder*="Search all foods" i]',
|
|
100
|
-
'input[placeholder*="Search" i]',
|
|
101
|
-
'input[placeholder*="food" i]',
|
|
102
|
-
'input.gwt-TextBox',
|
|
103
|
-
'input[type="text"]',
|
|
104
|
-
'input[type="search"]',
|
|
105
|
-
];
|
|
106
|
-
let searched = false;
|
|
107
|
-
for (const sel of searchSelectors) {
|
|
108
|
-
try {
|
|
109
|
-
const el = page.locator(sel);
|
|
110
|
-
if (await el.count() > 0) {
|
|
111
|
-
await el.first().click();
|
|
112
|
-
await page.waitForTimeout(200);
|
|
113
|
-
await el.first().fill('');
|
|
114
|
-
await page.keyboard.type(foodName, { delay: 50 });
|
|
115
|
-
searched = true;
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
|
-
} catch {}
|
|
119
|
-
}
|
|
120
|
-
if (!searched) {
|
|
121
|
-
return { success: false, error: 'Could not find food search bar in Add Food dialog' };
|
|
122
|
-
}
|
|
123
|
-
await page.waitForTimeout(300);
|
|
124
|
-
|
|
125
|
-
// Click SEARCH
|
|
126
|
-
await clickFirst([
|
|
127
|
-
'text="SEARCH"',
|
|
128
|
-
'button:has-text("SEARCH")',
|
|
129
|
-
'button:has-text("Search")',
|
|
130
|
-
], 'SEARCH button');
|
|
131
|
-
|
|
132
|
-
// Wait for results
|
|
133
|
-
try {
|
|
134
|
-
await page.waitForSelector('td:has-text("' + foodName + '")', { timeout: 8000 });
|
|
135
|
-
} catch {
|
|
136
|
-
return { success: false, error: 'No food found matching "' + foodName + '"' };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Select the search result
|
|
140
|
-
const resultSelectors = [
|
|
141
|
-
'td:has-text("' + foodName + '")',
|
|
142
|
-
'tr:has-text("' + foodName + '") td',
|
|
143
|
-
'.gwt-HTML:has-text("' + foodName + '")',
|
|
144
|
-
'div:has-text("' + foodName + '"):not(:has(input))',
|
|
145
|
-
];
|
|
146
|
-
let resultClicked = false;
|
|
147
|
-
for (const sel of resultSelectors) {
|
|
148
|
-
try {
|
|
149
|
-
const el = page.locator(sel);
|
|
150
|
-
if (await el.count() > 0) {
|
|
151
|
-
await el.first().click();
|
|
152
|
-
resultClicked = true;
|
|
153
|
-
break;
|
|
154
|
-
}
|
|
155
|
-
} catch {}
|
|
156
|
-
}
|
|
157
|
-
if (!resultClicked) {
|
|
158
|
-
return { success: false, error: 'No food found matching "' + foodName + '"' };
|
|
159
|
-
}
|
|
160
|
-
await page.waitForTimeout(200);
|
|
161
|
-
|
|
162
|
-
// Wait for the detail panel with serving size
|
|
163
|
-
try {
|
|
164
|
-
await page.waitForSelector('text="Serving Size"', { timeout: 5000 });
|
|
165
|
-
} catch {
|
|
166
|
-
return { success: false, error: 'Serving Size panel did not appear for "' + foodName + '"' };
|
|
167
|
-
}
|
|
168
|
-
await page.waitForTimeout(500);
|
|
169
|
-
|
|
170
|
-
// If servings != 1, update the serving size input
|
|
171
|
-
if (servingCount !== 1) {
|
|
172
|
-
let servingFilled = false;
|
|
173
|
-
try {
|
|
174
|
-
servingFilled = await page.evaluate((count) => {
|
|
175
|
-
const walker = document.createTreeWalker(
|
|
176
|
-
document.body,
|
|
177
|
-
NodeFilter.SHOW_TEXT,
|
|
178
|
-
{ acceptNode: (node) =>
|
|
179
|
-
node.textContent && node.textContent.trim() === 'Serving Size'
|
|
180
|
-
? NodeFilter.FILTER_ACCEPT
|
|
181
|
-
: NodeFilter.FILTER_REJECT
|
|
182
|
-
}
|
|
183
|
-
);
|
|
184
|
-
const textNode = walker.nextNode();
|
|
185
|
-
if (!textNode) return false;
|
|
186
|
-
|
|
187
|
-
let container = textNode.parentElement;
|
|
188
|
-
for (let i = 0; i < 5 && container; i++) {
|
|
189
|
-
const input = container.querySelector('input');
|
|
190
|
-
if (input) {
|
|
191
|
-
input.focus();
|
|
192
|
-
input.select();
|
|
193
|
-
const nativeSetter = Object.getOwnPropertyDescriptor(
|
|
194
|
-
window.HTMLInputElement.prototype, 'value'
|
|
195
|
-
).set;
|
|
196
|
-
nativeSetter.call(input, String(count));
|
|
197
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
198
|
-
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
container = container.parentElement;
|
|
202
|
-
}
|
|
203
|
-
return false;
|
|
204
|
-
}, servingCount);
|
|
205
|
-
} catch {}
|
|
206
|
-
|
|
207
|
-
if (!servingFilled) {
|
|
208
|
-
return { success: false, error: 'Could not update serving size for "' + foodName + '"' };
|
|
209
|
-
}
|
|
210
|
-
await page.waitForTimeout(500);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Click "ADD TO DIARY"
|
|
214
|
-
const addClicked = await clickFirst([
|
|
215
|
-
'button:has-text("ADD TO DIARY")',
|
|
216
|
-
'button:has-text("Add to Diary")',
|
|
217
|
-
'text="ADD TO DIARY"',
|
|
218
|
-
'text="Add to Diary"',
|
|
219
|
-
'button[type="submit"]',
|
|
220
|
-
], 'ADD TO DIARY button');
|
|
221
|
-
if (!addClicked) {
|
|
222
|
-
return { success: false, error: 'Could not find "Add to Diary" button' };
|
|
223
|
-
}
|
|
224
|
-
await page.waitForSelector('text="Add Food to Diary"', { state: 'hidden', timeout: 8000 }).catch(() => {});
|
|
225
|
-
await page.waitForTimeout(300);
|
|
52
|
+
${buildFoodDialogCode({ updateServingSize: true })}
|
|
226
53
|
|
|
227
54
|
return { success: true };
|
|
228
55
|
`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"log-food.js","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"log-food.js","sourceRoot":"","sources":["../../src/kernel/log-food.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AAEvD;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAmB;IAClD,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IAEvC,MAAM,SAAS,GAAG,IAAI;QACpB,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE;QAC5D,CAAC,CAAC,eAAe,CAAC;IAEpB,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,YAAY,GAAG,QAAQ,IAAI,CAAC,CAAC;IAEnC,OAAO;uBACc,QAAQ;wBACP,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC;2BACtB,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;EA0BrC,mBAAmB,CAAC,EAAE,iBAAiB,EAAE,IAAI,EAAE,CAAC;;;GAG/C,CAAC;AACJ,CAAC"}
|
package/dist/kernel/login.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
9
|
* Generate code that checks if the user is logged into Cronometer.
|
|
10
|
-
* Navigates to /#diary and checks if
|
|
10
|
+
* Navigates to /#diary and checks if login UI is still presented.
|
|
11
11
|
*/
|
|
12
12
|
export declare function buildLoginCheckCode(): string;
|
|
13
13
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,
|
|
1
|
+
{"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAc5C;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,IAAI,MAAM,CAKjD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAiH7E"}
|
package/dist/kernel/login.js
CHANGED
|
@@ -7,15 +7,21 @@
|
|
|
7
7
|
*/
|
|
8
8
|
/**
|
|
9
9
|
* Generate code that checks if the user is logged into Cronometer.
|
|
10
|
-
* Navigates to /#diary and checks if
|
|
10
|
+
* Navigates to /#diary and checks if login UI is still presented.
|
|
11
11
|
*/
|
|
12
12
|
export function buildLoginCheckCode() {
|
|
13
13
|
return `
|
|
14
14
|
await page.goto('https://cronometer.com/#diary', { waitUntil: 'domcontentloaded', timeout: 15000 });
|
|
15
15
|
await page.waitForLoadState('networkidle', { timeout: 10000 }).catch(() => {});
|
|
16
|
+
await page.waitForTimeout(1000);
|
|
16
17
|
const url = page.url();
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
${buildLoginPresentedCheckCode()}
|
|
19
|
+
const diaryPresented = await page.evaluate(() => {
|
|
20
|
+
return !!document.querySelector('i.diary-date-previous, i.diary-date-next') ||
|
|
21
|
+
/Energy\\s+\\d+\\.?\\d*\\s*kcal/i.test(document.body.innerText);
|
|
22
|
+
}).catch(() => false);
|
|
23
|
+
const isLoggedIn = url.includes('#diary') && !url.includes('/login') && !url.includes('/signin') && !loginPresented && diaryPresented;
|
|
24
|
+
return { success: true, loggedIn: isLoggedIn, url, loginPresented, diaryPresented };
|
|
19
25
|
`;
|
|
20
26
|
}
|
|
21
27
|
/**
|
|
@@ -118,7 +124,8 @@ export function buildAutoLoginCode(username, password) {
|
|
|
118
124
|
await page.waitForTimeout(500);
|
|
119
125
|
|
|
120
126
|
const url = page.url();
|
|
121
|
-
|
|
127
|
+
${buildLoginPresentedCheckCode()}
|
|
128
|
+
const loggedIn = !url.includes('/login') && !url.includes('/signin') && !loginPresented;
|
|
122
129
|
|
|
123
130
|
// If still on login page, check for error messages (rate limit, wrong creds, etc.)
|
|
124
131
|
let loginError = null;
|
|
@@ -145,4 +152,27 @@ export function buildAutoLoginCode(username, password) {
|
|
|
145
152
|
return { success: true, loggedIn, url, loginError };
|
|
146
153
|
`;
|
|
147
154
|
}
|
|
155
|
+
function buildLoginPresentedCheckCode() {
|
|
156
|
+
return `
|
|
157
|
+
const loginPresented = await page.evaluate(() => {
|
|
158
|
+
const visible = (el) => {
|
|
159
|
+
const style = window.getComputedStyle(el);
|
|
160
|
+
return style && style.visibility !== 'hidden' && style.display !== 'none' && el.getClientRects().length > 0;
|
|
161
|
+
};
|
|
162
|
+
const hasVisibleSelector = (selector) =>
|
|
163
|
+
Array.from(document.querySelectorAll(selector)).some((el) => visible(el));
|
|
164
|
+
const hasLoginInput =
|
|
165
|
+
hasVisibleSelector('input[type="email"], input[name="username"], input[name="email"], #email, #username') ||
|
|
166
|
+
hasVisibleSelector('input[type="password"], input[name="password"], #password');
|
|
167
|
+
const hasLoginAction = Array.from(document.querySelectorAll('a, button'))
|
|
168
|
+
.filter((el) => visible(el))
|
|
169
|
+
.some((el) => {
|
|
170
|
+
const text = el.textContent?.replace(/\\s+/g, ' ').trim().toLowerCase() ?? '';
|
|
171
|
+
return ['log in', 'login', 'sign in', 'signin'].includes(text);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
return hasLoginInput || hasLoginAction;
|
|
175
|
+
}).catch(() => false);
|
|
176
|
+
`;
|
|
177
|
+
}
|
|
148
178
|
//# sourceMappingURL=login.js.map
|
package/dist/kernel/login.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO
|
|
1
|
+
{"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/kernel/login.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;GAGG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO;;;;;MAKH,4BAA4B,EAAE;;;;;;;GAOjC,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,wBAAwB;IACtC,OAAO;;;GAGN,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAgB,EAAE,QAAgB;IACnE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IAE1C,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kCAiCyB,QAAQ;;;;;;;;;;;;;;;;;kCAiBR,QAAQ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;MAgCpC,4BAA4B,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BjC,CAAC;AACJ,CAAC;AAED,SAAS,4BAA4B;IACnC,OAAO;;;;;;;;;;;;;;;;;;;;GAoBN,CAAC;AACJ,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quick-add.d.ts","sourceRoot":"","sources":["../../src/kernel/quick-add.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"quick-add.d.ts","sourceRoot":"","sources":["../../src/kernel/quick-add.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAG9C;;;GAGG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAKrD,CAAC;AAEF;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAiG3D"}
|