重开仓库内容:改推五个技能(browser-harness / humanizer / humanizer-zh / product-planning / session-mechanism)
按授权清空原有内容后重新提交(原 oil-ui-pro 一并移出,可从历史恢复)。 browser-harness 剔除 .venv 等运行环境;根 .gitignore 补记 .venv/ 与 node_modules/。
This commit is contained in:
1 parent
9a152a1952
commit
777f7fe5d0
240 files changed
+52518
-4409
No files matched your search
@@ -0,0 +1,168 @@
|
||||
# Expedia — Browser Automation
|
||||
|
||||
Field-tested against expedia.co.in on 2026-04-27 using `browser-harness` CDP
|
||||
helpers (`goto`, `js`, `click`, `type_text`, `screenshot`).
|
||||
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
**Build your search via URL parameters, not the UI.** The date picker, destination
|
||||
autocomplete, and traveller widgets are fragile—coordinate clicks frequently
|
||||
dismiss them or mis-target. Encode everything you can into a `goto()` URL, then
|
||||
use JS clicks only for what the URL can't express (child ages, price filters).
|
||||
|
||||
---
|
||||
|
||||
## Hotel Search URL Template
|
||||
|
||||
```
|
||||
https://www.expedia.co.in/Hotel-Search?destination={DEST}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}&rooms={N}&adults={N}&children={N}&childrenAges={age1,age2,...}
|
||||
```
|
||||
|
||||
Example — 2 adults, 2 children (ages 5 and 7), Tokyo, June 2026:
|
||||
|
||||
```python
|
||||
goto("https://www.expedia.co.in/Hotel-Search?"
|
||||
"destination=Central+Tokyo,+Tokyo+Prefecture&"
|
||||
"startDate=2026-06-01&endDate=2026-06-07&"
|
||||
"rooms=1&adults=2&children=2&childrenAges=5,7")
|
||||
```
|
||||
|
||||
**Note:** `childrenAges` in the URL may not always populate the age dropdowns on
|
||||
the results page. Verify with a screenshot and set them via JS if needed.
|
||||
|
||||
---
|
||||
|
||||
## Date Picker — DO NOT USE
|
||||
|
||||
The calendar widget is extremely unreliable with coordinate-based clicks:
|
||||
|
||||
- Clicking a date cell frequently **closes the entire picker** instead of
|
||||
selecting the date.
|
||||
- The picker has month-navigation arrows that are tiny targets.
|
||||
- "Flexible dates" mode has a different DOM structure with pill-shaped month
|
||||
selectors that also mis-fire.
|
||||
- Dozens of retry attempts across multiple strategies all failed.
|
||||
|
||||
**Workaround:** Always pass dates via URL parameters (`startDate`, `endDate`).
|
||||
|
||||
---
|
||||
|
||||
## Travellers Widget
|
||||
|
||||
The travellers stepper panel works with JS `.click()` on the increment/decrement
|
||||
buttons.
|
||||
|
||||
### Opening the panel
|
||||
|
||||
```python
|
||||
js("""
|
||||
(()=>{
|
||||
let btn = document.querySelector('button[data-testid="travelers-field-trigger"]')
|
||||
|| [...document.querySelectorAll('button')].find(b => b.textContent.includes('traveller'));
|
||||
if(btn){ btn.click(); return 'opened'; }
|
||||
return 'not found';
|
||||
})()
|
||||
""")
|
||||
```
|
||||
|
||||
### Incrementing children count
|
||||
|
||||
```python
|
||||
js("""
|
||||
(()=>{
|
||||
let span = [...document.querySelectorAll('span')].find(s => s.textContent.trim() === 'Children');
|
||||
if(!span) return 'no Children label';
|
||||
let container = span.closest('div').parentElement;
|
||||
let buttons = container.querySelectorAll('button');
|
||||
let plus = buttons[buttons.length - 1]; // last button is "+"
|
||||
plus.click();
|
||||
return 'incremented';
|
||||
})()
|
||||
""")
|
||||
```
|
||||
|
||||
### Setting child ages
|
||||
|
||||
Child age dropdowns are `<select>` elements with `aria-label` like
|
||||
"Child 1 age", "Child 2 age", etc.
|
||||
|
||||
```python
|
||||
js("""
|
||||
(()=>{
|
||||
let selects = document.querySelectorAll('select');
|
||||
// selects[0] = Child 1 age, selects[1] = Child 2 age, etc.
|
||||
let setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
||||
selects[0].value = '5';
|
||||
selects[0].dispatchEvent(new Event('change', {bubbles:true}));
|
||||
selects[1].value = '7';
|
||||
selects[1].dispatchEvent(new Event('change', {bubbles:true}));
|
||||
return 'ages set';
|
||||
})()
|
||||
""")
|
||||
```
|
||||
|
||||
### Closing the panel
|
||||
|
||||
```python
|
||||
js("""
|
||||
(()=>{
|
||||
let btn = [...document.querySelectorAll('button')].find(b => b.textContent.trim() === 'Done');
|
||||
if(btn){ btn.click(); return 'done'; }
|
||||
return 'no Done button';
|
||||
})()
|
||||
""")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Price Filter
|
||||
|
||||
On the results page, the nightly-price filter has two text inputs and two range
|
||||
sliders.
|
||||
|
||||
| Element | Selector |
|
||||
|---------|----------|
|
||||
| Min text input | `#price-min` |
|
||||
| Max text input | `#price-max` |
|
||||
| Min range slider | `input[type="range"][aria-label*="Minimum"]` |
|
||||
| Max range slider | `input[type="range"][aria-label*="Maximum"]` |
|
||||
|
||||
### Setting max price
|
||||
|
||||
The most reliable method is to click the input, select all, type the value, and
|
||||
press Enter:
|
||||
|
||||
```python
|
||||
click(x, y) # coordinates of #price-max
|
||||
js("document.getElementById('price-max').select()")
|
||||
type_text("20000")
|
||||
press_key("Enter")
|
||||
```
|
||||
|
||||
Setting the value purely via JS (`dispatchEvent`) does trigger a re-search but
|
||||
coordinate-click + type is more reliable for actually applying the filter.
|
||||
|
||||
---
|
||||
|
||||
## Key Lessons
|
||||
|
||||
1. **URL-first strategy** — Encode destination, dates, room count, adults,
|
||||
children, and child ages in the URL. Only use UI interaction for things the
|
||||
URL cannot express.
|
||||
|
||||
2. **JS `.click()` over coordinate clicks** — For buttons inside panels
|
||||
(traveller stepper, Done), find elements by text/attribute and call `.click()`
|
||||
in JS. Coordinate clicks on overlay panels are unreliable.
|
||||
|
||||
3. **`dispatchEvent` with `{bubbles: true}`** — Required for React-controlled
|
||||
inputs (selects, text fields). Without bubbling, React state won't update.
|
||||
|
||||
4. **Wait after navigation** — After `goto()` or pressing Search, call
|
||||
`wait_for_load()` + `time.sleep(3)` before interacting. Expedia loads
|
||||
results asynchronously.
|
||||
|
||||
5. **Indian locale** — `expedia.co.in` shows prices in ₹ (INR). The price
|
||||
filter values include the ₹ symbol and commas in the text input but the
|
||||
underlying range slider uses plain integers.
|
||||
Reference in new issue
Block a user