td-ai-tools 1.2.0 → 1.2.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/package.json +1 -1
- package/skills/README.md +2 -0
- package/skills/client-overview/SKILL.md +93 -0
- package/skills/client-overview/agents/openai.yaml +4 -0
- package/skills/client-overview/scripts/branch_client_overview_context.py +317 -0
- package/skills/debugging-ios-webkit/SKILL.md +60 -0
- package/skills/debugging-ios-webkit/references/device.md +62 -0
- package/skills/debugging-ios-webkit/references/playwright.md +60 -0
- package/skills/debugging-ios-webkit/references/simulator.md +76 -0
- package/skills/debugging-ios-webkit/scripts/device_eval.py +62 -0
- package/skills/debugging-ios-webkit/scripts/device_experiment.py +99 -0
- package/skills/debugging-ios-webkit/scripts/device_snapshot.py +62 -0
- package/skills/record-changes/SKILL.md +3 -3
package/package.json
CHANGED
package/skills/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
- `browser-validation`: Before completing a task validate frontend or template changes in a real browser with the Playwright-CLI…
|
|
8
8
|
- `cache-reset`: Clear and warm Laravel and Statamic caches (including Statamic Glide image caches) after content or template…
|
|
9
9
|
- `car-ticket-generator`: Generate a ticket for the codex-auto-runner queue
|
|
10
|
+
- `client-overview`: Generate a client-facing markdown report that summarizes all changes on the current branch against the…
|
|
11
|
+
- `debugging-ios-webkit`: Debugs iOS Safari/Chrome-iOS rendering bugs — stale paints, viewport/browser-chrome clipping, mobile-only CSS…
|
|
10
12
|
- `everhour-basecamp-estimates`: Bulk update Everhour task estimates from a Basecamp todo or todolist URL, then append bracketed hours to the…
|
|
11
13
|
- `forge-cli`: Manage Laravel Forge servers, sites, and provisioned resources from the terminal with the Laravel Forge CLI,…
|
|
12
14
|
- `horizon-component-migration`: Bundle Shopify Horizon components into a migration package for a different theme, including recursive…
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: client-overview
|
|
3
|
+
version: 1.0.0
|
|
4
|
+
description: Generate a client-facing markdown report that summarizes all changes on the current branch against the primary development branch for a non-technical audience, with accessibility changes explained through end-user experience, accessibility benchmark categories, and practical impact.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Client Overview
|
|
8
|
+
|
|
9
|
+
Generate a markdown report for a client or stakeholder audience. Cover all meaningful branch changes, but refer to the work as an update (client's will not understand the term branch). Give extra attention to accessibility-related changes by explaining what people experience when using the affected feature, as well as which WCAG guidelines the change addresses.
|
|
10
|
+
|
|
11
|
+
## Workflow
|
|
12
|
+
|
|
13
|
+
### 1. Collect Branch Context
|
|
14
|
+
|
|
15
|
+
Run the bundled script first:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
python3 .agents/skills/client-overview/scripts/branch_client_overview_context.py
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
If the user specifies another comparison branch, pass it explicitly:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python3 .agents/skills/client-overview/scripts/branch_client_overview_context.py --base release/x.y
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Default output folder: `docs/`, if the repo does not have a docs folder use the root instead. Do not use or reference the changes.md file, as that is intended for internal technical users.
|
|
28
|
+
|
|
29
|
+
### 2. Inspect the Actual Changes
|
|
30
|
+
|
|
31
|
+
Use the script output to identify changed files, then inspect relevant diffs and source files with `git diff` and targeted reads. Do not rely on branch names or file names alone.
|
|
32
|
+
|
|
33
|
+
Prioritize:
|
|
34
|
+
|
|
35
|
+
- User-facing behavior, layout, copy, navigation, forms, search, cart, checkout, account, and content changes
|
|
36
|
+
- Accessibility-relevant markup, ARIA, labels, headings, landmarks, alt text, focus handling, keyboard behavior, validation, contrast, spacing, motion, responsive behavior, and interactive controls
|
|
37
|
+
- CSS and JavaScript changes that affect visibility, focus, scroll behavior, modals, drawers, menus, accordions, sliders, media, filters, or dynamic content
|
|
38
|
+
- CMS settings or schema changes that alter what site admins can configure for visitors
|
|
39
|
+
|
|
40
|
+
Do not summarize the report file edit itself as branch work. If the branch includes tooling-only changes, include them only when they affect the non-technical reader's experience or confidence in the release.
|
|
41
|
+
|
|
42
|
+
### 3. Translate Accessibility Changes
|
|
43
|
+
|
|
44
|
+
For each accessibility-relevant change, write in terms of:
|
|
45
|
+
|
|
46
|
+
- **Benchmark category:** Use plain category names such as keyboard access, screen reader clarity, focus visibility, form labels and errors, color contrast, reduced motion, responsive reflow, touch target usability, readable content, or predictable navigation. Add WCAG references only if the code clearly maps to them.
|
|
47
|
+
- **User experience:** State what someone with a disability, temporary impairment, or assistive technology setup can now do, understand, avoid, or recover from.
|
|
48
|
+
- **Affected journey:** Name the feature or page area in shopper terms, such as product options, search filters, cart drawer, checkout path, navigation menu, newsletter signup, or collection browsing.
|
|
49
|
+
- **Before/after effect:** Describe the practical improvement without blaming previous work. Example: "Keyboard shoppers can now see which control is active while moving through the menu."
|
|
50
|
+
|
|
51
|
+
Avoid implementation-heavy language in the main summary. Keep file paths, selectors, variables, and component names out of the file unless there is a clear edge case requiring them.
|
|
52
|
+
|
|
53
|
+
### 4. Write the Report
|
|
54
|
+
|
|
55
|
+
Example entry structure:
|
|
56
|
+
|
|
57
|
+
```markdown
|
|
58
|
+
## <short user-facing title>
|
|
59
|
+
**Date:** YYYY-MM-DD
|
|
60
|
+
|
|
61
|
+
### At a Glance
|
|
62
|
+
- <plain-language summary of the update outcome>
|
|
63
|
+
|
|
64
|
+
### What Changed for Users
|
|
65
|
+
| Area | User-facing change | Why it matters |
|
|
66
|
+
|------|--------------------|----------------|
|
|
67
|
+
|
|
68
|
+
### Accessibility Experience
|
|
69
|
+
| Benchmark category | Affected experience | User impact |
|
|
70
|
+
|--------------------|---------------------|-------------|
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Guidelines:
|
|
74
|
+
|
|
75
|
+
- Title the entry by the experience improved, not the branch name.
|
|
76
|
+
- Write for clients, support, QA, designers, and stakeholders who may not read code.
|
|
77
|
+
- In the output file, refer to the branch as "this update" or "the update"; do not call it "the branch".
|
|
78
|
+
- Do not refer to yourself, the agent, or the writing process in the output file. Avoid first-person process statements such as "I reviewed," "I checked," "this summary," or "this report."
|
|
79
|
+
- Prefer "people using keyboard navigation" over "keyboard users" when the sentence is about lived experience.
|
|
80
|
+
- Prefer "screen reader announces..." only when the code change truly affects accessible names, roles, states, landmarks, headings, or live regions.
|
|
81
|
+
|
|
82
|
+
### 5. Verify Before Finishing
|
|
83
|
+
|
|
84
|
+
Before wrapping up:
|
|
85
|
+
|
|
86
|
+
- Re-read the new markdown entry in context.
|
|
87
|
+
- Confirm every listed file or area appears in the branch diff.
|
|
88
|
+
- Confirm the report describes observed code behavior, not intended behavior inferred from a branch name.
|
|
89
|
+
- Confirm accessibility claims are tied to a benchmark category and an affected user experience.
|
|
90
|
+
|
|
91
|
+
## Resource
|
|
92
|
+
|
|
93
|
+
- `scripts/branch_client_overview_context.py`: Resolve the best available base branch and print branch file changes with accessibility inspection hints.
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_BASE_CANDIDATES = ("main", "origin/main", "master", "origin/master")
|
|
11
|
+
|
|
12
|
+
ACCESSIBILITY_HINTS = (
|
|
13
|
+
(
|
|
14
|
+
"keyboard access and focus behavior",
|
|
15
|
+
(
|
|
16
|
+
"focus",
|
|
17
|
+
"tabindex",
|
|
18
|
+
"keydown",
|
|
19
|
+
"keyup",
|
|
20
|
+
"keypress",
|
|
21
|
+
"escape",
|
|
22
|
+
"drawer",
|
|
23
|
+
"modal",
|
|
24
|
+
"menu",
|
|
25
|
+
"accordion",
|
|
26
|
+
"slider",
|
|
27
|
+
"carousel",
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
(
|
|
31
|
+
"screen reader clarity and semantic structure",
|
|
32
|
+
(
|
|
33
|
+
"aria",
|
|
34
|
+
"role",
|
|
35
|
+
"label",
|
|
36
|
+
"heading",
|
|
37
|
+
"h1",
|
|
38
|
+
"h2",
|
|
39
|
+
"h3",
|
|
40
|
+
"landmark",
|
|
41
|
+
"sr-only",
|
|
42
|
+
"visually-hidden",
|
|
43
|
+
"alt",
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
(
|
|
47
|
+
"form labels, instructions, and error recovery",
|
|
48
|
+
(
|
|
49
|
+
"form",
|
|
50
|
+
"input",
|
|
51
|
+
"select",
|
|
52
|
+
"textarea",
|
|
53
|
+
"error",
|
|
54
|
+
"invalid",
|
|
55
|
+
"required",
|
|
56
|
+
"newsletter",
|
|
57
|
+
"contact",
|
|
58
|
+
),
|
|
59
|
+
),
|
|
60
|
+
(
|
|
61
|
+
"color contrast, readable content, and visual state",
|
|
62
|
+
(
|
|
63
|
+
"color",
|
|
64
|
+
"contrast",
|
|
65
|
+
"opacity",
|
|
66
|
+
"background",
|
|
67
|
+
"foreground",
|
|
68
|
+
"text",
|
|
69
|
+
"font",
|
|
70
|
+
"hover",
|
|
71
|
+
"active",
|
|
72
|
+
),
|
|
73
|
+
),
|
|
74
|
+
(
|
|
75
|
+
"responsive reflow, zoom, and touch target usability",
|
|
76
|
+
(
|
|
77
|
+
"mobile",
|
|
78
|
+
"responsive",
|
|
79
|
+
"breakpoint",
|
|
80
|
+
"media",
|
|
81
|
+
"width",
|
|
82
|
+
"height",
|
|
83
|
+
"spacing",
|
|
84
|
+
"padding",
|
|
85
|
+
"touch",
|
|
86
|
+
),
|
|
87
|
+
),
|
|
88
|
+
(
|
|
89
|
+
"motion, animation, and scroll comfort",
|
|
90
|
+
(
|
|
91
|
+
"motion",
|
|
92
|
+
"animation",
|
|
93
|
+
"transition",
|
|
94
|
+
"scroll",
|
|
95
|
+
"autoplay",
|
|
96
|
+
"video",
|
|
97
|
+
"prefers-reduced-motion",
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def run_git(*args: str) -> str:
|
|
104
|
+
completed = subprocess.run(
|
|
105
|
+
["git", *args],
|
|
106
|
+
check=True,
|
|
107
|
+
capture_output=True,
|
|
108
|
+
text=True,
|
|
109
|
+
)
|
|
110
|
+
return completed.stdout.strip()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def ref_exists(ref: str) -> bool:
|
|
114
|
+
completed = subprocess.run(
|
|
115
|
+
["git", "rev-parse", "--verify", f"{ref}^{{commit}}"],
|
|
116
|
+
capture_output=True,
|
|
117
|
+
text=True,
|
|
118
|
+
)
|
|
119
|
+
return completed.returncode == 0
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def resolve_base_ref(explicit_base: str | None) -> str:
|
|
123
|
+
if explicit_base:
|
|
124
|
+
if not ref_exists(explicit_base):
|
|
125
|
+
raise SystemExit(f"Base ref not found: {explicit_base}")
|
|
126
|
+
return explicit_base
|
|
127
|
+
|
|
128
|
+
for candidate in DEFAULT_BASE_CANDIDATES:
|
|
129
|
+
if ref_exists(candidate):
|
|
130
|
+
return candidate
|
|
131
|
+
|
|
132
|
+
searched = ", ".join(DEFAULT_BASE_CANDIDATES)
|
|
133
|
+
raise SystemExit(f"No base ref found. Tried: {searched}")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def parse_name_status(output: str) -> list[dict[str, str]]:
|
|
137
|
+
files = []
|
|
138
|
+
for line in output.splitlines():
|
|
139
|
+
if not line:
|
|
140
|
+
continue
|
|
141
|
+
parts = line.split("\t")
|
|
142
|
+
status = parts[0]
|
|
143
|
+
|
|
144
|
+
if status.startswith("R") and len(parts) >= 3:
|
|
145
|
+
files.append(
|
|
146
|
+
{
|
|
147
|
+
"status": status,
|
|
148
|
+
"path": parts[2],
|
|
149
|
+
"old_path": parts[1],
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
continue
|
|
153
|
+
|
|
154
|
+
path = parts[1] if len(parts) > 1 else ""
|
|
155
|
+
files.append({"status": status, "path": path, "old_path": ""})
|
|
156
|
+
return files
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def parse_numstat(output: str) -> dict[str, dict[str, str]]:
|
|
160
|
+
stats = {}
|
|
161
|
+
for line in output.splitlines():
|
|
162
|
+
if not line:
|
|
163
|
+
continue
|
|
164
|
+
parts = line.split("\t")
|
|
165
|
+
if len(parts) < 3:
|
|
166
|
+
continue
|
|
167
|
+
additions, deletions, path = parts[0], parts[1], parts[2]
|
|
168
|
+
stats[path] = {"additions": additions, "deletions": deletions}
|
|
169
|
+
return stats
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def resolve_stats(
|
|
173
|
+
numstat: dict[str, dict[str, str]], path: str, old_path: str
|
|
174
|
+
) -> dict[str, str] | None:
|
|
175
|
+
stats = numstat.get(path)
|
|
176
|
+
if stats or not old_path:
|
|
177
|
+
return stats
|
|
178
|
+
|
|
179
|
+
path_dir = os.path.dirname(path)
|
|
180
|
+
old_dir = os.path.dirname(old_path)
|
|
181
|
+
path_name = os.path.basename(path)
|
|
182
|
+
old_name = os.path.basename(old_path)
|
|
183
|
+
|
|
184
|
+
for candidate_path, candidate_stats in numstat.items():
|
|
185
|
+
if old_path in candidate_path and path in candidate_path:
|
|
186
|
+
return candidate_stats
|
|
187
|
+
if (
|
|
188
|
+
path_dir == old_dir
|
|
189
|
+
and path_dir
|
|
190
|
+
and candidate_path.startswith(f"{path_dir}/")
|
|
191
|
+
and old_name in candidate_path
|
|
192
|
+
and path_name in candidate_path
|
|
193
|
+
):
|
|
194
|
+
return candidate_stats
|
|
195
|
+
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def accessibility_hints_for(path: str) -> list[str]:
|
|
200
|
+
path_lower = path.lower()
|
|
201
|
+
_, ext = os.path.splitext(path_lower)
|
|
202
|
+
hints = []
|
|
203
|
+
|
|
204
|
+
for label, keywords in ACCESSIBILITY_HINTS:
|
|
205
|
+
if any(keyword in path_lower for keyword in keywords):
|
|
206
|
+
hints.append(label)
|
|
207
|
+
|
|
208
|
+
if ext in {".css", ".scss", ".sass"}:
|
|
209
|
+
hints.extend(
|
|
210
|
+
[
|
|
211
|
+
"color contrast, readable content, and visual state",
|
|
212
|
+
"responsive reflow, zoom, and touch target usability",
|
|
213
|
+
]
|
|
214
|
+
)
|
|
215
|
+
elif ext in {".js", ".ts", ".jsx", ".tsx"}:
|
|
216
|
+
hints.extend(
|
|
217
|
+
[
|
|
218
|
+
"keyboard access and focus behavior",
|
|
219
|
+
"motion, animation, and scroll comfort",
|
|
220
|
+
]
|
|
221
|
+
)
|
|
222
|
+
elif ext in {".liquid", ".html", ".vue", ".svelte"}:
|
|
223
|
+
hints.extend(
|
|
224
|
+
[
|
|
225
|
+
"screen reader clarity and semantic structure",
|
|
226
|
+
"form labels, instructions, and error recovery",
|
|
227
|
+
]
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return sorted(set(hints))
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def build_context(base_ref: str) -> dict[str, object]:
|
|
234
|
+
current_branch = run_git("branch", "--show-current")
|
|
235
|
+
merge_base = run_git("merge-base", base_ref, "HEAD")
|
|
236
|
+
shortstat = run_git("diff", "--shortstat", merge_base, "HEAD")
|
|
237
|
+
name_status = parse_name_status(
|
|
238
|
+
run_git("diff", "--name-status", "--find-renames", merge_base, "HEAD")
|
|
239
|
+
)
|
|
240
|
+
numstat = parse_numstat(run_git("diff", "--numstat", "--find-renames", merge_base, "HEAD"))
|
|
241
|
+
|
|
242
|
+
for entry in name_status:
|
|
243
|
+
stats = resolve_stats(numstat, entry["path"], entry["old_path"])
|
|
244
|
+
entry["additions"] = stats["additions"] if stats else "?"
|
|
245
|
+
entry["deletions"] = stats["deletions"] if stats else "?"
|
|
246
|
+
path_for_hints = f"{entry['old_path']} {entry['path']}".strip()
|
|
247
|
+
entry["accessibility_hints"] = accessibility_hints_for(path_for_hints)
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
"current_branch": current_branch,
|
|
251
|
+
"base_ref": base_ref,
|
|
252
|
+
"merge_base": merge_base,
|
|
253
|
+
"shortstat": shortstat,
|
|
254
|
+
"files": name_status,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def print_markdown(context: dict[str, object]) -> None:
|
|
259
|
+
print("# Branch Client Overview Context")
|
|
260
|
+
print()
|
|
261
|
+
print(f"- Current branch: `{context['current_branch']}`")
|
|
262
|
+
print(f"- Base ref: `{context['base_ref']}`")
|
|
263
|
+
print(f"- Merge base: `{context['merge_base']}`")
|
|
264
|
+
print(f"- Summary: {context['shortstat'] or 'No changes detected'}")
|
|
265
|
+
print()
|
|
266
|
+
|
|
267
|
+
files = context["files"]
|
|
268
|
+
if not files:
|
|
269
|
+
print("No changed files detected.")
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
print("| Status | Path | +/- | Accessibility inspection hints |")
|
|
273
|
+
print("|--------|------|-----|----------------------------------|")
|
|
274
|
+
|
|
275
|
+
for entry in files:
|
|
276
|
+
status = entry["status"]
|
|
277
|
+
path = entry["path"]
|
|
278
|
+
if entry["old_path"]:
|
|
279
|
+
path = f"{entry['old_path']} -> {entry['path']}"
|
|
280
|
+
delta = f"+{entry['additions']} / -{entry['deletions']}"
|
|
281
|
+
hints = ", ".join(entry["accessibility_hints"]) or "No obvious hint from file path"
|
|
282
|
+
print(f"| `{status}` | `{path}` | {delta} | {hints} |")
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def main() -> int:
|
|
286
|
+
parser = argparse.ArgumentParser(
|
|
287
|
+
description="Resolve a base branch and print context for client-facing change reports."
|
|
288
|
+
)
|
|
289
|
+
parser.add_argument(
|
|
290
|
+
"--base",
|
|
291
|
+
help="Explicit base ref to compare against. Defaults to main/origin-main or master/origin-master fallback.",
|
|
292
|
+
)
|
|
293
|
+
parser.add_argument(
|
|
294
|
+
"--format",
|
|
295
|
+
choices=("markdown", "json"),
|
|
296
|
+
default="markdown",
|
|
297
|
+
help="Output format.",
|
|
298
|
+
)
|
|
299
|
+
args = parser.parse_args()
|
|
300
|
+
|
|
301
|
+
base_ref = resolve_base_ref(args.base)
|
|
302
|
+
context = build_context(base_ref)
|
|
303
|
+
|
|
304
|
+
if args.format == "json":
|
|
305
|
+
print(json.dumps(context, indent=2))
|
|
306
|
+
else:
|
|
307
|
+
print_markdown(context)
|
|
308
|
+
|
|
309
|
+
return 0
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
if __name__ == "__main__":
|
|
313
|
+
try:
|
|
314
|
+
raise SystemExit(main())
|
|
315
|
+
except subprocess.CalledProcessError as error:
|
|
316
|
+
sys.stderr.write(error.stderr or str(error))
|
|
317
|
+
raise SystemExit(error.returncode)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: debugging-ios-webkit
|
|
3
|
+
description: Debugs iOS Safari/Chrome-iOS rendering bugs — stale paints, viewport/browser-chrome clipping, mobile-only CSS issues — in whichever environment is specified. Pass "playwright" (fast emulated WebKit), "simulator" (Xcode iOS Simulator via simctl), or "device" (USB-connected physical iPhone via pymobiledevice3 Web Inspector). Use when a bug is reported "only on iPhone", when the user wants to test on a real device or in the simulator, or when styles are correct but pixels are wrong.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Debugging iOS WebKit Rendering Issues
|
|
7
|
+
|
|
8
|
+
Every iOS browser is WebKit — "Safari and Chrome on iOS" is one engine.
|
|
9
|
+
|
|
10
|
+
## Choose your environment
|
|
11
|
+
|
|
12
|
+
Read ONLY the reference for the environment requested (or per the guidance
|
|
13
|
+
below), then follow it:
|
|
14
|
+
|
|
15
|
+
- **[references/playwright.md](references/playwright.md)** — emulated WebKit,
|
|
16
|
+
headless, seconds per iteration. CSS cascade, layout, load-order races,
|
|
17
|
+
scripted touch loops. Cannot reproduce iOS compositor/GPU or browser-chrome
|
|
18
|
+
behavior.
|
|
19
|
+
- **[references/simulator.md](references/simulator.md)** — real iOS WebKit +
|
|
20
|
+
real Safari chrome via `simctl`. Viewport units (svh/dvh), safe-area,
|
|
21
|
+
pixel-accurate screenshots. No touch synthesis — drive state via URL params
|
|
22
|
+
or temporary page hooks.
|
|
23
|
+
- **[references/device.md](references/device.md)** — physical iPhone over
|
|
24
|
+
USB. Attach to the live (broken) page, evaluate JS, capture render
|
|
25
|
+
snapshots, run stepped on-screen repair experiments with the person
|
|
26
|
+
watching. The whole truth; required for stale-compositor bugs.
|
|
27
|
+
|
|
28
|
+
If the user didn't specify: start with playwright for speed, escalate to
|
|
29
|
+
simulator when the bug involves viewport/chrome behavior, and to device when
|
|
30
|
+
neither reproduces it or someone has the broken state on a phone right now.
|
|
31
|
+
|
|
32
|
+
## Shared diagnostic: layout bug vs paint bug (read this regardless)
|
|
33
|
+
|
|
34
|
+
In the broken state, measure **three** things: computed styles + bounding
|
|
35
|
+
rects (JS), a render-tree snapshot (`Page.snapshotRect` on device, or a fresh
|
|
36
|
+
screenshot in playwright/simulator), and what the eyes/screen actually show.
|
|
37
|
+
|
|
38
|
+
- **Styles or rects wrong** → layout/cascade bug. Fix CSS. Iterate at the
|
|
39
|
+
playwright level.
|
|
40
|
+
- **Styles, rects, AND snapshot correct while the screen shows garbage** →
|
|
41
|
+
the compositor is presenting a stale GPU texture. Fix with a paint
|
|
42
|
+
invalidation in JS, not with CSS. The cheapest reliable one is the opacity
|
|
43
|
+
nudge; to avoid a visible pop, dirty the layer *before* the element becomes
|
|
44
|
+
visible and keep nudging every animation frame through any entrance
|
|
45
|
+
transition:
|
|
46
|
+
|
|
47
|
+
```js
|
|
48
|
+
el.style.opacity = '0.99';
|
|
49
|
+
const start = performance.now();
|
|
50
|
+
(function tick() {
|
|
51
|
+
if (performance.now() - start < 350) {
|
|
52
|
+
el.style.opacity = el.style.opacity === '0.99' ? '1' : '0.99';
|
|
53
|
+
requestAnimationFrame(tick);
|
|
54
|
+
} else { el.style.opacity = ''; }
|
|
55
|
+
})();
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
A painted background measuring exactly the element's *content-box* (instead
|
|
59
|
+
of border-box) is the signature of unpainted padding — stale layer or native
|
|
60
|
+
form-control painter (`-webkit-appearance` missing at first style pass).
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
|
|
2
|
+
# Debugging on a Real iOS Device
|
|
3
|
+
|
|
4
|
+
Attach to the live page over the cable — including a currently-broken state
|
|
5
|
+
someone is looking at — without reloading or losing it.
|
|
6
|
+
|
|
7
|
+
## Setup (once)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
python3.12 -m venv venv && venv/bin/pip install pymobiledevice3
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Python ≥ 3.10 required (Xcode's 3.9 breaks the CLI). On the phone:
|
|
14
|
+
Settings → (Apps →) Safari → Advanced → **Web Inspector** ON. Plug in, trust.
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
venv/bin/python -m pymobiledevice3 webinspector opened-tabs # list tabs
|
|
20
|
+
python scripts/device_eval.py mystore.com '<js expression>' # eval in tab
|
|
21
|
+
python scripts/device_snapshot.py mystore.com out.png # render snapshot
|
|
22
|
+
python scripts/device_experiment.py mystore.com '.my-footer' # live experiments
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Scripts take a URL substring to pick the tab. First sanity check: confirm
|
|
26
|
+
`location.href` and `Shopify.theme` — reporters are frequently on production
|
|
27
|
+
or the wrong theme, which explains many "still happening" reports instantly.
|
|
28
|
+
|
|
29
|
+
## Rules that prevent silent hangs
|
|
30
|
+
|
|
31
|
+
- **One inspector client at a time.** Close Mac Safari's Develop-menu
|
|
32
|
+
inspector, or your attach hangs with no error.
|
|
33
|
+
- The target tab must be **foreground** with the **screen unlocked**.
|
|
34
|
+
- iOS 17+ wraps the protocol in the `Target` domain: sessions need
|
|
35
|
+
`wait_target=True` and responses arrive inside
|
|
36
|
+
`Target.dispatchMessageFromTarget` (the scripts handle both).
|
|
37
|
+
- `ios_webkit_debug_proxy` lists tabs on modern iOS but its eval channel is
|
|
38
|
+
broken — use pymobiledevice3.
|
|
39
|
+
|
|
40
|
+
## Applying the shared diagnostic here
|
|
41
|
+
|
|
42
|
+
Run the layout-vs-paint diagnostic from SKILL.md using `device_eval.py`
|
|
43
|
+
(rects + computed styles) and `device_snapshot.py` (render-tree snapshot,
|
|
44
|
+
via `Page.snapshotRect` — it bypasses stale compositor layers, so a correct
|
|
45
|
+
snapshot against a broken screen is the paint-bug proof).
|
|
46
|
+
|
|
47
|
+
## Live repair experiments (stale-paint bugs)
|
|
48
|
+
|
|
49
|
+
Don't guess which invalidation works — bisect it on the actual broken state
|
|
50
|
+
with the reporter watching (`device_experiment.py`):
|
|
51
|
+
|
|
52
|
+
1. A countdown banner tells them to reproduce the bug ("break it now,
|
|
53
|
+
steps start in 20s") — **warn them before starting so they're watching**.
|
|
54
|
+
2. Numbered colored banners announce each repair candidate ~5s apart:
|
|
55
|
+
banner-only sanity → opacity nudge → `translateZ(0)` layer →
|
|
56
|
+
`display:none` rebuild → class re-toggle → scroll nudge.
|
|
57
|
+
3. They report the step number that visually healed the element. Wire exactly
|
|
58
|
+
that operation into the code path that precedes the broken state.
|
|
59
|
+
|
|
60
|
+
The winning invalidation (usually the opacity nudge — see the shared
|
|
61
|
+
diagnostic in SKILL.md for the production-safe anti-pop version) goes into
|
|
62
|
+
the code path that precedes the broken state.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
|
|
2
|
+
# Debugging with Playwright WebKit
|
|
3
|
+
|
|
4
|
+
Every iOS browser is WebKit (Safari and Chrome-iOS share the engine), so
|
|
5
|
+
Playwright's WebKit build catches most engine-level bugs in seconds, headless.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
const { webkit, devices } = require('playwright');
|
|
11
|
+
const browser = await webkit.launch();
|
|
12
|
+
const ctx = await browser.newContext({ ...devices['iPhone 14 Pro'], hasTouch: true });
|
|
13
|
+
const page = await ctx.newPage();
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
One-off setup: `npm i playwright` in a scratch dir (e.g. `/tmp/repro`).
|
|
17
|
+
|
|
18
|
+
## Instructions
|
|
19
|
+
|
|
20
|
+
1. **Measure two ways, always.** Computed styles/rects via `page.evaluate`
|
|
21
|
+
AND painted pixels via `page.screenshot()` + a pixel scan. A paint bug
|
|
22
|
+
shows correct computed values with wrong pixels; if you only read
|
|
23
|
+
`getComputedStyle` you will wrongly conclude "works for me".
|
|
24
|
+
|
|
25
|
+
2. **Real touch, not synthetic clicks.** Use `locator.tap()` (requires
|
|
26
|
+
`hasTouch: true`) when the bug involves touch-driven UI.
|
|
27
|
+
|
|
28
|
+
3. **Interaction loops.** Before declaring something unreproducible, loop the
|
|
29
|
+
failing interaction 30x, measuring on every iteration:
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
for (let i = 1; i <= 30; i++) {
|
|
33
|
+
await page.locator('.js-open-trigger').tap(); // your theme's selector
|
|
34
|
+
await page.waitForTimeout(400);
|
|
35
|
+
const m = await page.evaluate(() => {
|
|
36
|
+
const el = document.querySelector('#target-element');
|
|
37
|
+
const r = el.getBoundingClientRect();
|
|
38
|
+
return { w: Math.round(r.width), h: Math.round(r.height), pad: getComputedStyle(el).padding };
|
|
39
|
+
});
|
|
40
|
+
console.log(i, JSON.stringify(m));
|
|
41
|
+
await page.locator('.js-close-trigger').tap();
|
|
42
|
+
await page.waitForTimeout(300);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
4. **Slow-network CSS races.** Delay one stylesheet with `page.route()`.
|
|
47
|
+
Gotcha: a setup navigation warms the HTTP cache and the route never fires
|
|
48
|
+
on the test navigation — `route.abort()` the asset during setup, then
|
|
49
|
+
switch the handler to delay on the real run.
|
|
50
|
+
|
|
51
|
+
5. **Test against the same origin the reporter used** (CDN preview vs local
|
|
52
|
+
proxy) — asset latency and compiled bundles differ per page and origin.
|
|
53
|
+
|
|
54
|
+
## When this level is not enough
|
|
55
|
+
|
|
56
|
+
Playwright WebKit renders everything from a healthy desktop compositor. If
|
|
57
|
+
computed styles and screenshots are clean here but devices still show the bug,
|
|
58
|
+
it lives in iOS-specific territory: dynamic viewport (`dvh`) lag, browser
|
|
59
|
+
chrome show/hide, native form-control painters, or stale GPU textures.
|
|
60
|
+
Escalate to references/simulator.md.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
|
|
2
|
+
# Debugging in the iOS Simulator
|
|
3
|
+
|
|
4
|
+
Real iOS WebKit + real Safari chrome, scriptable from the terminal. The one
|
|
5
|
+
hard limitation: **you cannot synthesize taps** (`simctl` has no touch API,
|
|
6
|
+
and macOS blocks synthetic clicks without Accessibility permission).
|
|
7
|
+
|
|
8
|
+
## Quick Start
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
xcrun simctl list devices available # find a device + UDID
|
|
12
|
+
open -a Simulator --args -CurrentDeviceUDID <UDID>
|
|
13
|
+
xcrun simctl bootstatus <UDID> -b # wait for boot
|
|
14
|
+
xcrun simctl openurl booted "https://site.example/page"
|
|
15
|
+
xcrun simctl io booted screenshot /tmp/shot.png
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Driving state without touch
|
|
19
|
+
|
|
20
|
+
- **URL parameters the page already supports** — e.g. many themes open the
|
|
21
|
+
cart drawer on load with `?cart=open`; check the theme's JS for what it reads.
|
|
22
|
+
- **Temporary debug hooks** — add a query-param-gated script to the page that
|
|
23
|
+
auto-runs the interaction sequence, sync it to a dev theme, load with the
|
|
24
|
+
param, screenshot on a timer, then remove the hook:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
if (new URLSearchParams(location.search).has('debug_cycles')) {
|
|
28
|
+
window.addEventListener('load', async () => {
|
|
29
|
+
const t = (ms) => new Promise(r => setTimeout(r, ms));
|
|
30
|
+
// example: cycle whatever component is under test
|
|
31
|
+
const d = document.querySelector('cart-drawer');
|
|
32
|
+
await t(2000); d.open(); await t(2500); d.close(); await t(1500); d.open();
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- **App-switch nudge** (forces visibility/viewport recalc):
|
|
38
|
+
`xcrun simctl launch booted com.apple.Preferences; sleep 2; xcrun simctl launch booted com.apple.mobilesafari`
|
|
39
|
+
|
|
40
|
+
## Measuring screenshots
|
|
41
|
+
|
|
42
|
+
Screenshots are 3x scale on Pro-class devices — divide pixels by 3 for CSS px.
|
|
43
|
+
Pixel-scan with PIL rather than eyeballing:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from PIL import Image
|
|
47
|
+
im = Image.open('/tmp/shot.png').convert('RGB')
|
|
48
|
+
# find rows containing the element's known background colour, measure the band
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Comparing the measured painted box against the element's expected border-box
|
|
52
|
+
identifies which box was painted: a background at exactly content-box size
|
|
53
|
+
means padding wasn't painted (stale layer or native form-control painter).
|
|
54
|
+
|
|
55
|
+
## Calibrating viewport units
|
|
56
|
+
|
|
57
|
+
The simulator reaches your Mac's `localhost` directly. Serve a test page
|
|
58
|
+
(`python3 -m http.server 8899`) with fixed-position divs of `100svh`,
|
|
59
|
+
`100dvh`, `100lvh`, `100%` height and a `bottom: 0` marker, screenshot, and
|
|
60
|
+
measure where each ends. This settles "which viewport is this unit tracking"
|
|
61
|
+
arguments with data.
|
|
62
|
+
|
|
63
|
+
## Shopify-specific gotchas
|
|
64
|
+
|
|
65
|
+
- Preview/development themes show the **Draft bar**, which overlays the
|
|
66
|
+
bottom ~46pt of the viewport and covers bottom-anchored CTAs — it looks
|
|
67
|
+
exactly like a layout bug. Append `&pb=0` to hide it before judging.
|
|
68
|
+
- Add items to the cart with
|
|
69
|
+
`openurl booted ".../cart/add?id=<variant>&quantity=1&return_to=/cart"`
|
|
70
|
+
(a `/cart/<variant>:1` permalink jumps straight to checkout — not useful).
|
|
71
|
+
|
|
72
|
+
## When this level is not enough
|
|
73
|
+
|
|
74
|
+
If the bug needs real touch gestures, device GPU behavior, or you have the
|
|
75
|
+
broken state live on someone's phone, use references/device.md. For fast CSS iteration first, use
|
|
76
|
+
references/playwright.md.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Evaluate JavaScript in a live Safari tab on a USB-connected iOS device.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python device_eval.py <url-substring> ['<js-expression>']
|
|
6
|
+
|
|
7
|
+
The expression runs in the page and its return value is printed. Wrap DOM
|
|
8
|
+
reads in an IIFE returning JSON.stringify(...) for structured output.
|
|
9
|
+
Defaults to a generic element diagnostic if no expression is given.
|
|
10
|
+
|
|
11
|
+
Requires: pip install pymobiledevice3 (Python >= 3.10)
|
|
12
|
+
Phone: Settings -> Safari -> Advanced -> Web Inspector ON, tab foreground,
|
|
13
|
+
screen unlocked, and no other inspector attached (close Mac Safari Develop).
|
|
14
|
+
"""
|
|
15
|
+
import asyncio
|
|
16
|
+
import sys
|
|
17
|
+
import uuid
|
|
18
|
+
|
|
19
|
+
from pymobiledevice3.lockdown import create_using_usbmux
|
|
20
|
+
from pymobiledevice3.services.webinspector import WebinspectorService
|
|
21
|
+
from pymobiledevice3.services.web_protocol.inspector_session import InspectorSession
|
|
22
|
+
from pymobiledevice3.services.web_protocol.session_protocol import SessionProtocol
|
|
23
|
+
|
|
24
|
+
DEFAULT_EXPR = r"""(() => {
|
|
25
|
+
return JSON.stringify({
|
|
26
|
+
href: location.href.slice(0, 120),
|
|
27
|
+
theme: (window.Shopify && Shopify.theme) ? Shopify.theme : undefined,
|
|
28
|
+
vv: { h: Math.round(visualViewport.height), ih: innerHeight },
|
|
29
|
+
});
|
|
30
|
+
})()"""
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def attach(inspector, match):
|
|
34
|
+
pages = await inspector.get_open_application_pages(timeout=2)
|
|
35
|
+
ap = next((p for p in pages if match in (getattr(p.page, 'web_url', '') or '')), None)
|
|
36
|
+
if ap is None:
|
|
37
|
+
urls = [getattr(p.page, 'web_url', None) for p in pages]
|
|
38
|
+
raise SystemExit(f'no tab matching {match!r}; open tabs: {urls}')
|
|
39
|
+
session_id = str(uuid.uuid4()).upper()
|
|
40
|
+
# wait_target=True is required on iOS 17+ (Target-domain wrapped protocol)
|
|
41
|
+
session = await asyncio.wait_for(
|
|
42
|
+
InspectorSession.create(
|
|
43
|
+
SessionProtocol(inspector, session_id, ap.application, ap.page, method_prefix=""),
|
|
44
|
+
wait_target=True),
|
|
45
|
+
timeout=10)
|
|
46
|
+
await session.runtime_enable()
|
|
47
|
+
return session
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def main():
|
|
51
|
+
match = sys.argv[1] if len(sys.argv) > 1 else 'http'
|
|
52
|
+
expr = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_EXPR
|
|
53
|
+
lockdown = await create_using_usbmux()
|
|
54
|
+
inspector = WebinspectorService(lockdown=lockdown)
|
|
55
|
+
async with inspector:
|
|
56
|
+
session = await attach(inspector, match)
|
|
57
|
+
result = await asyncio.wait_for(session.runtime_evaluate(expr, return_by_value=True), timeout=15)
|
|
58
|
+
print(result)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == '__main__':
|
|
62
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run stepped repair experiments on a live broken page while a human watches
|
|
3
|
+
the device. Each step is announced with a colored banner injected into the
|
|
4
|
+
page; the watcher reports which step number visually fixed the element.
|
|
5
|
+
|
|
6
|
+
Usage:
|
|
7
|
+
python device_experiment.py <url-substring> [target-css-selector]
|
|
8
|
+
|
|
9
|
+
Edit STEPS for the element/bug at hand. The winning step becomes the
|
|
10
|
+
permanent fix, wired into the code path that precedes the broken state.
|
|
11
|
+
"""
|
|
12
|
+
import asyncio
|
|
13
|
+
import sys
|
|
14
|
+
import uuid
|
|
15
|
+
|
|
16
|
+
from pymobiledevice3.lockdown import create_using_usbmux
|
|
17
|
+
from pymobiledevice3.services.webinspector import WebinspectorService
|
|
18
|
+
from pymobiledevice3.services.web_protocol.inspector_session import InspectorSession
|
|
19
|
+
from pymobiledevice3.services.web_protocol.session_protocol import SessionProtocol
|
|
20
|
+
|
|
21
|
+
TARGET = sys.argv[2] if len(sys.argv) > 2 else 'body'
|
|
22
|
+
|
|
23
|
+
STEPS = [
|
|
24
|
+
("STEP 1: banner only (sanity)", "void 0"),
|
|
25
|
+
("STEP 2: opacity nudge", f"""
|
|
26
|
+
(() => {{ const el = document.querySelector('{TARGET}'); if (!el) return;
|
|
27
|
+
el.style.opacity = '0.99';
|
|
28
|
+
requestAnimationFrame(() => {{ el.style.opacity = ''; }}); }})()"""),
|
|
29
|
+
("STEP 3: own compositing layer", f"""
|
|
30
|
+
(() => {{ const el = document.querySelector('{TARGET}'); if (!el) return;
|
|
31
|
+
el.style.transform = 'translateZ(0)'; }})()"""),
|
|
32
|
+
("STEP 4: display rebuild", f"""
|
|
33
|
+
(() => {{ const el = document.querySelector('{TARGET}'); if (!el) return;
|
|
34
|
+
el.style.display = 'none'; void el.offsetHeight; el.style.display = ''; }})()"""),
|
|
35
|
+
("STEP 5: reflow via class re-toggle", f"""
|
|
36
|
+
(() => {{ const el = document.querySelector('{TARGET}'); if (!el) return;
|
|
37
|
+
const p = el.parentElement; p.classList.add('td-exp-retoggle');
|
|
38
|
+
void p.offsetHeight; p.classList.remove('td-exp-retoggle'); }})()"""),
|
|
39
|
+
("STEP 6: scroll nudge", """
|
|
40
|
+
(() => { const s = document.scrollingElement; s.scrollTop += 1; s.scrollTop -= 1; })()"""),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
BANNER = """
|
|
44
|
+
(() => {
|
|
45
|
+
let el = document.getElementById('td-exp-banner');
|
|
46
|
+
if (!el) {
|
|
47
|
+
el = document.createElement('div');
|
|
48
|
+
el.id = 'td-exp-banner';
|
|
49
|
+
el.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:2147483647;padding:18px;font:bold 24px -apple-system;text-align:center;color:#fff;';
|
|
50
|
+
document.body.appendChild(el);
|
|
51
|
+
}
|
|
52
|
+
el.style.background = COLOR;
|
|
53
|
+
el.textContent = LABEL;
|
|
54
|
+
})()
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
COLORS = ['#c0392b', '#d35400', '#f39c12', '#27ae60', '#2980b9', '#8e44ad']
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def main():
|
|
61
|
+
match = sys.argv[1] if len(sys.argv) > 1 else 'http'
|
|
62
|
+
lockdown = await create_using_usbmux()
|
|
63
|
+
inspector = WebinspectorService(lockdown=lockdown)
|
|
64
|
+
async with inspector:
|
|
65
|
+
pages = await inspector.get_open_application_pages(timeout=2)
|
|
66
|
+
ap = next((p for p in pages if match in (getattr(p.page, 'web_url', '') or '')), None)
|
|
67
|
+
if ap is None:
|
|
68
|
+
raise SystemExit(f'no tab matching {match!r}')
|
|
69
|
+
session_id = str(uuid.uuid4()).upper()
|
|
70
|
+
session = await asyncio.wait_for(
|
|
71
|
+
InspectorSession.create(
|
|
72
|
+
SessionProtocol(inspector, session_id, ap.application, ap.page, method_prefix=""),
|
|
73
|
+
wait_target=True),
|
|
74
|
+
timeout=10)
|
|
75
|
+
await session.runtime_enable()
|
|
76
|
+
|
|
77
|
+
async def banner(color, label):
|
|
78
|
+
await session.runtime_evaluate(BANNER.replace('COLOR', repr(color)).replace('LABEL', repr(label)))
|
|
79
|
+
|
|
80
|
+
# Lead-in so the watcher can reproduce the broken state first
|
|
81
|
+
for remaining in range(20, 0, -5):
|
|
82
|
+
await banner('#111', f'BREAK IT NOW — reproduce the bug. Steps start in {remaining}s')
|
|
83
|
+
print(f'lead-in {remaining}s', flush=True)
|
|
84
|
+
await asyncio.sleep(5)
|
|
85
|
+
|
|
86
|
+
for i, (label, code) in enumerate(STEPS):
|
|
87
|
+
await banner(COLORS[i % len(COLORS)], label + ' — fixed?')
|
|
88
|
+
await session.runtime_evaluate(code)
|
|
89
|
+
print('ran', label, flush=True)
|
|
90
|
+
await asyncio.sleep(5)
|
|
91
|
+
|
|
92
|
+
await banner('#111', 'DONE — report which step fixed it')
|
|
93
|
+
await asyncio.sleep(6)
|
|
94
|
+
await session.runtime_evaluate("document.getElementById('td-exp-banner')?.remove()")
|
|
95
|
+
print('done', flush=True)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == '__main__':
|
|
99
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Capture a Page.snapshotRect PNG from a live Safari tab on a USB device.
|
|
3
|
+
|
|
4
|
+
Usage:
|
|
5
|
+
python device_snapshot.py <url-substring> [out.png] [x y w h]
|
|
6
|
+
|
|
7
|
+
IMPORTANT: the snapshot repaints from the render tree. If the snapshot looks
|
|
8
|
+
CORRECT while the device screen looks BROKEN, the bug is a stale compositor
|
|
9
|
+
texture (paint bug), not a layout bug — fix with a paint invalidation (e.g.
|
|
10
|
+
opacity nudge), not with CSS.
|
|
11
|
+
"""
|
|
12
|
+
import asyncio
|
|
13
|
+
import base64
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
import uuid
|
|
17
|
+
|
|
18
|
+
from pymobiledevice3.lockdown import create_using_usbmux
|
|
19
|
+
from pymobiledevice3.services.webinspector import WebinspectorService
|
|
20
|
+
from pymobiledevice3.services.web_protocol.inspector_session import InspectorSession
|
|
21
|
+
from pymobiledevice3.services.web_protocol.session_protocol import SessionProtocol
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
async def main():
|
|
25
|
+
match = sys.argv[1] if len(sys.argv) > 1 else 'http'
|
|
26
|
+
out = sys.argv[2] if len(sys.argv) > 2 else 'snapshot.png'
|
|
27
|
+
x, y, w, h = (int(a) for a in sys.argv[3:7]) if len(sys.argv) > 6 else (0, 0, 430, 932)
|
|
28
|
+
|
|
29
|
+
lockdown = await create_using_usbmux()
|
|
30
|
+
inspector = WebinspectorService(lockdown=lockdown)
|
|
31
|
+
async with inspector:
|
|
32
|
+
pages = await inspector.get_open_application_pages(timeout=2)
|
|
33
|
+
ap = next((p for p in pages if match in (getattr(p.page, 'web_url', '') or '')), None)
|
|
34
|
+
if ap is None:
|
|
35
|
+
raise SystemExit(f'no tab matching {match!r}')
|
|
36
|
+
session_id = str(uuid.uuid4()).upper()
|
|
37
|
+
session = await asyncio.wait_for(
|
|
38
|
+
InspectorSession.create(
|
|
39
|
+
SessionProtocol(inspector, session_id, ap.application, ap.page, method_prefix=""),
|
|
40
|
+
wait_target=True),
|
|
41
|
+
timeout=10)
|
|
42
|
+
|
|
43
|
+
mid = await session.send_message_to_target({'method': 'Page.enable', 'params': {}})
|
|
44
|
+
await asyncio.wait_for(session.receive_response_by_id(mid), timeout=10)
|
|
45
|
+
|
|
46
|
+
mid = await session.send_message_to_target(
|
|
47
|
+
{'method': 'Page.snapshotRect',
|
|
48
|
+
'params': {'x': x, 'y': y, 'width': w, 'height': h, 'coordinateSystem': 'Viewport'}})
|
|
49
|
+
res = await asyncio.wait_for(session.receive_response_by_id(mid), timeout=15)
|
|
50
|
+
# iOS 17+ wraps responses in the Target domain — unwrap
|
|
51
|
+
if res.get('method') == 'Target.dispatchMessageFromTarget':
|
|
52
|
+
res = json.loads(res['params']['message'])
|
|
53
|
+
data = res.get('result', res).get('dataURL', '')
|
|
54
|
+
if not data.startswith('data:image/png;base64,'):
|
|
55
|
+
raise SystemExit(f'snapshot failed: {json.dumps(res)[:200]}')
|
|
56
|
+
with open(out, 'wb') as f:
|
|
57
|
+
f.write(base64.b64decode(data.split(',', 1)[1]))
|
|
58
|
+
print('saved', out)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
if __name__ == '__main__':
|
|
62
|
+
asyncio.run(main())
|
|
@@ -33,8 +33,8 @@ Use the script output to identify changed files, then inspect the relevant diffs
|
|
|
33
33
|
Prioritize:
|
|
34
34
|
|
|
35
35
|
- User-facing behavior changes
|
|
36
|
-
-
|
|
37
|
-
- CSS or markup changes that affect
|
|
36
|
+
- CMS setting/schema changes
|
|
37
|
+
- CSS or markup changes that affect rendering
|
|
38
38
|
- Renamed files and vendor-theme hotspots
|
|
39
39
|
|
|
40
40
|
Do not summarize the `docs/changes.md` edit itself as part of the branch work. If the branch contains unrelated skill or tooling files, either omit them from the changelog entry or separate them clearly when they are relevant to the project's maintenance history.
|
|
@@ -57,7 +57,7 @@ Guidelines:
|
|
|
57
57
|
- Make the title describe the feature or fix, not the branch name.
|
|
58
58
|
- Write `Purpose` in plain language with outcome-focused bullets.
|
|
59
59
|
- Use the `Files changed` table to explain why each file matters.
|
|
60
|
-
- Call out non-`td-` theme or vendor files in a separate subsection when
|
|
60
|
+
- Call out non-`td-` theme or vendor files in a separate subsection when in a Shopify project.
|
|
61
61
|
- Mention renamed files explicitly.
|
|
62
62
|
- Keep `Upgrade impact` brief and concrete.
|
|
63
63
|
- Use `Notes` for implementation details, edge cases, or assumptions.
|