mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
feat: web-native specs + per-platform doctor (#49)
* feat(doctor): per-platform check sets + --platform flag
Replaces unconditional defaultDoctorChecks with doctorChecksFor(platform);
web-only users no longer see scary FAILs for adb/emulator/java/sidecar.
* feat(testrun): add Preflight() before sidecar/driver setup
Mobile platforms get a friendly install hint pointing at
`sanderling doctor --platform=<p>` instead of `fork/exec java: not found`.
Web is a no-op.
* refactor(chrome): split tag (HTML name) from class (CSS classList)
Hierarchy attributes now expose HTML tag under 'tag' and CSS classes
under 'class', stopping the conflation of the two.
* feat(chrome): translate legacy string selectors to CSS/XPath
TapSelector now maps id:/desc:/descPrefix:/testTag:/etc. through
TranslateStringSelector. Unknown prefixes pass through to a CSS
attribute selector so a future Maestro key works without a release.
* feat(trace): add WriteHTML + Step.HTMLAvailable
Per-step HTML lives in <run>/html/step-NNNNN.html so trace.jsonl stays
line-greppable on apps with hundreds-of-KB DOMs.
* feat(driver): add WebDriver capability + chrome implementation
WebDriver exposes InstallBundle/EvaluateExtractors/NextActionFromV8/Document
for the V8-native web tick path. Mobile drivers stay binary-compatible.
* feat(verifier): OverrideExtractorValues for V8-driven extractors
Web tick path runs extractor bodies in V8 against the real DOM, then
overrides goja-side .current slots so LTL predicates evaluate against
those values. Mobile callers can pass nil for a no-op.
* feat(spec): add WebState + camelCase attribute aliases
WebState extends State with live `document`/`window` for V8-side web
extractors. KnownAttrSelectors gains camelCase aliases (contentDescription,
ariaLabel, testID, etc.) so cross-framework specs autocomplete.
* feat(runner): per-tick HTML capture for WebDriver-capable drivers
Type-asserts driver.WebDriver and writes <run>/html/step-NNNNN.html in
parallel with screenshot/hierarchy/metrics. Step.HTMLAvailable flips so
the inspect UI can hide the html tab on mobile runs.
* feat(inspect): serveHTML route under /api/runs/<id>/html/<name>
Mirrors serveScreenshot path validation; rejects traversal segments and
unknown extensions. text/html content-type so the iframe renders cleanly.
* feat(bundler): BundleWeb + V8-side runtime shim
web-runtime.ts installs globalThis.__sanderling__ with extractor / action
registries, plus __sanderlingExtractors__ + __sanderlingNextAction__
globals. BundleWeb composes user spec + runtime under esbuild's
PlatformBrowser into one IIFE.
* feat(runner): V8 extractor overrides + V8 action source for WebDriver
When the driver implements WebDriver, the runner sources extractor values
from V8 (real DOM) and the next action from the V8-side action generator.
LTL property predicates still run host-side in goja.
* feat(testrun): bundle + install web runtime when platform=web
BundleWeb composes the user spec with web-runtime.ts; the chrome driver
installs the resulting IIFE via Page.AddScriptToEvaluateOnNewDocument
post-Launch so the per-tick V8 extractor + action evaluation can begin
on step 1.
* feat(inspect-ui): hierarchy + html panels in run detail
HierarchyPanel renders the captured DOM/AX tree with a filter input.
HtmlPanel renders the per-step HTML in an iframe (sandboxed) with a
toggle to view source. HTML tab only shows when the step actually has
HTML captured.
* fix(folio-web): drop aria-label data-carrier abuse
Account cards now expose data-account-id + data-balance attrs and use a
human-readable aria-label. total-balance / ledger / ledger-balance carry
data-cents and data-txn-count instead of stuffing values into title.
Spec rewritten to read structured attrs via object-form selectors.
* chore: rebuild inspect-ui dist + folio-web .gitignore
Embeds the new HierarchyPanel + HtmlPanel into the inspect-ui dist that
ships with sanderling. Adds folio-web/.gitignore so generated runs/
don't leak into commits.
* revert(trace): drop WriteHTML + Step.HTMLAvailable
Screenshots already cover inspection; HTML capture bloats disk by
50-200MB per run with no payoff.
* revert(runner): drop per-tick HTML capture
Removes captureHTML helper and its three call sites; HTMLAvailable
flag no longer set on Step.
* revert(driver): drop WebDriver.Document
Document was only consumed by the runner's HTML capture which is gone.
* revert(inspect): drop /html route
Removes htmlPathPattern, serveHTML, and the dispatch block that called
it; HTML capture no longer exists on disk.
* revert(inspect-ui): drop htmlUrl + html_available type
API surface no longer needs the HTML route; Step.html_available has no
producer.
* revert(inspect-ui): drop HtmlPanel + html tab
Removes the iframe-based HTML viewer and its before/after tab wiring
from RunDetail.
* test(inspect-ui): drop htmlUrl test, add @types/bun
Pulls bun-types into tsconfig so api.test.ts (which uses bun:test)
typechecks; this was broken from the original feature commit.
* chore: rebuild inspect-ui dist without HtmlPanel
Embedded SPA bundle no longer ships the iframe HTML viewer.
* fix(web-runtime): retry action resolution + implement taps/swipes
V8-side runtime previously returned null when weighted picked a
generator that returned [] (page-gated), causing 80%+ of ticks on
narrow routes to emit no action and no post-screenshot. Now retries
up to 16x like goja, and the taps/swipes builtins query the live DOM
for clickable elements / dispatch random swipes instead of returning
null.
* fix(web-runtime): drop swipe, restrict pressKey to browser-meaningful keys
Web has no swipe gesture, so swipes dispatched pointer events into empty
divs. Make swipe() and the swipes builtin no-op. For PressKey, replace
the always-"back" choice with a random pick from {enter, tab, escape,
up, down, left, right} - keys that have real semantics in a browser.
* chore(folio-web): drop swipes from action root
Web runtime no-ops Swipe; remove the import and weighted entry so the
spec doesn't request actions that won't fire.
* fix(inspect-ui): correct HierarchyPanel CSS variable names
Tokens --surface-1/--surface-2/--text-secondary/--border-subtle don't
exist in tokens.css, so sticky thead had no background and tag/bounds
text fell back to inherited color. Map to the canonical --surface,
--surface-elevated, --text-muted, --border that other panels use.
* fix(chrome): correct PressKey mappings to chromedp/kb constants
Old keyMap had "home":"\x00" (NUL byte) and arrow keys mapped to
random punctuation runes (\x25-\x28 = % & ' () instead of arrow
keys. "escape" was missing entirely while the V8 runtime emits it.
Drop back/home (no browser navigation semantics) and route the
remaining keys through chromedp/kb constants so they actually
dispatch as the named keys.
* fix(cli): -h/--help exits 0 instead of error code
parseDoctorArgs hand-rolled its own flag loop and surfaced help text
as an error; parseTestArgs used flag.ContinueOnError but propagated
flag.ErrHelp to main() which printed "error: flag: help requested"
and exited 1.
Switch parseDoctorArgs to flag.NewFlagSet matching parseTestArgs, then
recognise flag.ErrHelp in main() so all subcommands exit 0 on -h.
* fix(chrome): harden cssEscape for control chars + use [class~=]
Previous cssEscape only handled " and \, leaving NUL/newlines/control
chars to break out of the CSS string literal. Port the CSSOM string
serialization rules: NUL becomes U+FFFD, control chars become \HEX,
quotes/backslashes get escaped.
Class selector switched from `.x` (which would need separate identifier
escaping) to `[class~="x"]`, which is also semantically correct for
multi-class elements.
* fix(web-runtime): use CSS.escape and validate tag-name selectors
The previous cssEscape only handled " and \, leaving newlines/control
chars to break out of attribute string literals. Delegate to the
platform CSS.escape per CSSOM spec.
The `tag` selector branch returned the bare value through cssEscape,
which doesn't prevent pseudo-classes (`*:hover`) from injecting into
the surrounding selector. Add a positive whitelist; values that don't
match a tag-name pattern collapse to a never-matching `:not(*)`.
Also switch class selectors to `[class~="..."]` to remove the only
identifier-context use of cssEscape.
* fix(chrome): validate attribute name in unknown-prefix branch
A selector like `foo]:has(*),body[x:value` previously produced
[foo]:has(*),body[x="..."], a syntactically valid CSS selector that
escaped the attribute match and selected `body`. Reject anything that
isn't a plain HTML attribute name.
* fix(selectors): emit valid XPath 1.0 string literals via concat()
Both the Go translator and the V8 runtime escaped " by prepending \,
which XPath 1.0 doesn't accept (its string literals have no escape
syntax). A `text:` value containing a quote produced malformed XPath
that chromedp/document.evaluate rejected.
Use the standard concat() composition: when the value contains both
' and ", split on " and join with `, '"', ` so each fragment is
wrapped in single or double quotes individually.
* fix(runtime): surface unresolved action targets instead of dropping silently
serializeAction emitted {x:0,y:0} via `?? 0` whenever a Tap/InputText/Swipe
target failed to resolve to coordinates. The runner then collapsed those
to ErrNoAction, so every selector typo became a silent no-op tick.
Have the runtime return null on unresolved targets and log a console
warning (visible via chromedp's runtime listener). Drop the now-redundant
{0,0} -> ErrNoAction guard so a deliberate Tap at the origin actually
fires.
* fix(runner): use errgroup-bound ctx so siblings cancel on failure
The errgroup's bound ctx was discarded; goroutines closed over the
outer ctx, so neither a sibling failure nor the future ability to
propagate per-step cancellation reached the V8 extractor's CDP
round-trip. Switch closures to gctx and document why Wait()'s error
is intentionally discarded.
* fix(chrome): propagate caller ctx cancellation to CDP calls
InstallBundle, EvaluateExtractors, NextActionFromV8 ignored the caller
ctx and ran chromedp.Run on d.tabCtx alone, so step deadlines and
Ctrl-C couldn't interrupt an in-flight CDP round-trip on a hung tab.
Add a runCtx helper that derives a chromedp-bound context which also
cancels when the caller's ctx cancels, and route the three V8 entry
points through it.
* fix(verifier): tolerate out-of-range override indices
A single stale index from V8 aborted the entire override map, so any
valid entries alongside it were dropped and verification ran on stale
extractor values. V8 and goja register from the same bundle so a
mismatch is unusual but recoverable.
Skip out-of-range entries instead of erroring, and return the skipped
count so the runner logs the mismatch without losing valid overrides.
* test(verifier): cover object-shaped extractor overrides
Existing tests only override scalars (777, 200), so a future jsonToJSValue
regression around nested object propagation would slip through. Lock down
the contract: a JSON object override should make {attrs.testTag, balance}
readable from goja predicates.
* fix(web-runtime): lock global runtime hooks against page shadowing
AddScriptToEvaluateOnNewDocument runs first, but a page script can still
delete or replace window.__sanderling{,Extractors__,NextAction__} between
install and host invocation. Define them as non-writable, non-configurable
properties so any attempt to shadow them throws in strict mode rather than
silently breaking the run.
* perf(web-runtime): cache randomTap candidate DOM scan per tick
The 16-attempt retry loop in __sanderlingNextAction__ called
randomTap repeatedly; each call ran querySelectorAll over a-button-
input-... and re-flushed layout per match via getBoundingClientRect.
On heavy SPA routes that's the per-tick budget gone.
Cache the scan in a module-level slot, reset at the top of each
__sanderlingNextAction__ invocation so the cache doesn't outlive a tick.
* fix(web-runtime): cap sanitize recursion to prevent stack overflow
State exposes document and window (per WebState in types.ts). A user
extractor returning either crashes the runtime via stack overflow on
the circular DOM/Window references. Track seen objects in a WeakSet
and bail at depth 32 so the worst case becomes a truncated value, not
a process kill.
* fix(web-runtime): enforce pressKey allowlist in factory
The factory accepted any string while randomPressKey only emitted
enter/tab/escape/arrows. A spec emitting pressKey({key:"home"}) would
flow through to the chrome driver, which rejects unsupported keys with
a runtime error mid-step. Reject at the factory so the spec author
sees the failure where it originates.
* chore(chrome): drop dead bundleSource/bundleMu
bundleSource was written under bundleMu but never read. Either remove it
or wire a re-install path; remove until the second is actually needed.
* fix(chrome): use strconv.Atoi for extractor key parsing
fmt.Sscanf("%d", ...) silently accepts trailing garbage like "3abc"
as 3. strconv.Atoi rejects the same input outright, so a malformed
key surfaces as an error instead of a wrong-bucket override.
* fix(doctor): raise per-check timeout to 15s for chromium launch
5s could time out the headless chromium check on cold CI. Most checks
finish in milliseconds, so a longer ceiling doesn't slow real
failures.
* fix(runner): trust V8 coordinates for InputText, even at origin
resolveCoordinates required strict positive X/Y, so a V8-emitted
InputText for an element at viewport (0, *) or (*, 0) skipped the
focus tap and typed into whatever was focused. Distinguish the
selector-driven path (mobile) from the coords-only path (web V8) so
edge coordinates are honored without breaking the existing tree-lookup
fallback.
Add applyAction tests covering both the typical web case and the (0,0)
edge case.
* test(bundler): lock down deterministic output across builds
The review flagged map-iteration nondeterminism as a possible cause of
unstable bundle SHAs. Empirically esbuild's Define handling is order-
independent (parallel substitution rules), so output is already stable.
Add a regression test that builds 10x with multiple Defines and asserts
SHA equality so any future change that introduces ordering surfaces.
This commit is contained in:
38 files changed
+2244
-139
No files matched your search
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -11,6 +12,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/chromedp/chromedp"
|
||||||
|
|
||||||
"github.com/priyanshujain/sanderling/internal/sidecar"
|
"github.com/priyanshujain/sanderling/internal/sidecar"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,7 +22,31 @@ type doctorCheck struct {
|
|||||||
Run func(ctx context.Context) error
|
Run func(ctx context.Context) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultDoctorChecks() []doctorCheck {
|
// doctorChecksFor returns the host-readiness checks for a target platform.
|
||||||
|
// "all" returns the union (deduped by name) so the legacy zero-arg `doctor`
|
||||||
|
// behaviour keeps surfacing every platform's prerequisites.
|
||||||
|
func doctorChecksFor(platform string) []doctorCheck {
|
||||||
|
switch platform {
|
||||||
|
case "web":
|
||||||
|
return webChecks()
|
||||||
|
case "android":
|
||||||
|
return androidChecks()
|
||||||
|
case "ios":
|
||||||
|
return iosChecks()
|
||||||
|
case "all":
|
||||||
|
return allChecks()
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func webChecks() []doctorCheck {
|
||||||
|
return []doctorCheck{
|
||||||
|
{Name: "headless chromium can launch", Run: checkChromiumLaunch},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func androidChecks() []doctorCheck {
|
||||||
return []doctorCheck{
|
return []doctorCheck{
|
||||||
{Name: "adb on PATH", Run: checkExecutableOnPath("adb")},
|
{Name: "adb on PATH", Run: checkExecutableOnPath("adb")},
|
||||||
{Name: "emulator on PATH or under ANDROID_HOME", Run: checkEmulator},
|
{Name: "emulator on PATH or under ANDROID_HOME", Run: checkEmulator},
|
||||||
@@ -28,6 +55,49 @@ func defaultDoctorChecks() []doctorCheck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func iosChecks() []doctorCheck {
|
||||||
|
return []doctorCheck{
|
||||||
|
{Name: "xcrun on PATH", Run: checkExecutableOnPath("xcrun")},
|
||||||
|
{Name: "simctl on PATH", Run: checkExecutableOnPath("simctl")},
|
||||||
|
{Name: "java 17+ on PATH", Run: checkJavaVersion},
|
||||||
|
{Name: "sidecar JAR is real (not placeholder)", Run: checkSidecarJAR},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func allChecks() []doctorCheck {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
var combined []doctorCheck
|
||||||
|
for _, group := range [][]doctorCheck{webChecks(), androidChecks(), iosChecks()} {
|
||||||
|
for _, c := range group {
|
||||||
|
if seen[c.Name] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[c.Name] = true
|
||||||
|
combined = append(combined, c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return combined
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkChromiumLaunch boots a headless chromium under chromedp's default
|
||||||
|
// allocator, opens a blank tab, and tears down. Confirms the bundled CDP
|
||||||
|
// surface plus a working Chromium binary path.
|
||||||
|
func checkChromiumLaunch(ctx context.Context) error {
|
||||||
|
allocCtx, allocCancel := chromedp.NewExecAllocator(ctx,
|
||||||
|
append(chromedp.DefaultExecAllocatorOptions[:],
|
||||||
|
chromedp.Flag("headless", true),
|
||||||
|
chromedp.Flag("disable-gpu", true),
|
||||||
|
)...,
|
||||||
|
)
|
||||||
|
defer allocCancel()
|
||||||
|
tabCtx, tabCancel := chromedp.NewContext(allocCtx)
|
||||||
|
defer tabCancel()
|
||||||
|
if err := chromedp.Run(tabCtx, chromedp.Navigate("about:blank")); err != nil {
|
||||||
|
return fmt.Errorf("chromium launch: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func checkSidecarJAR(_ context.Context) error {
|
func checkSidecarJAR(_ context.Context) error {
|
||||||
if sidecar.IsPlaceholder() {
|
if sidecar.IsPlaceholder() {
|
||||||
return fmt.Errorf("placeholder JAR embedded; run `make sidecar && make sanderling` to embed the real fat JAR")
|
return fmt.Errorf("placeholder JAR embedded; run `make sidecar && make sanderling` to embed the real fat JAR")
|
||||||
@@ -38,10 +108,36 @@ func checkSidecarJAR(_ context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type doctorOptions struct {
|
||||||
|
platform string
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDoctorArgs(args []string, stderr io.Writer) (doctorOptions, error) {
|
||||||
|
flagSet := flag.NewFlagSet("doctor", flag.ContinueOnError)
|
||||||
|
flagSet.SetOutput(stderr)
|
||||||
|
var options doctorOptions
|
||||||
|
flagSet.StringVar(&options.platform, "platform", "all", "target platform: web, android, ios, all")
|
||||||
|
if err := flagSet.Parse(args); err != nil {
|
||||||
|
return doctorOptions{}, err
|
||||||
|
}
|
||||||
|
switch options.platform {
|
||||||
|
case "web", "android", "ios", "all":
|
||||||
|
return options, nil
|
||||||
|
default:
|
||||||
|
return doctorOptions{}, fmt.Errorf("unsupported platform: %q (web, android, ios, all)", options.platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// doctorCheckTimeout bounds a single host-readiness check. Most checks (exec
|
||||||
|
// lookups, file stats, java -version) finish in milliseconds, but
|
||||||
|
// checkChromiumLaunch boots a real browser and can exceed 5s on a cold CI
|
||||||
|
// host - 15s leaves headroom without making real failures feel hung.
|
||||||
|
const doctorCheckTimeout = 15 * time.Second
|
||||||
|
|
||||||
func runDoctorChecks(ctx context.Context, checks []doctorCheck, stdout io.Writer) error {
|
func runDoctorChecks(ctx context.Context, checks []doctorCheck, stdout io.Writer) error {
|
||||||
failures := 0
|
failures := 0
|
||||||
for _, check := range checks {
|
for _, check := range checks {
|
||||||
callCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
|
callCtx, cancel := context.WithTimeout(ctx, doctorCheckTimeout)
|
||||||
err := check.Run(callCtx)
|
err := check.Run(callCtx)
|
||||||
cancel()
|
cancel()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -81,3 +83,102 @@ func TestCheckExecutableOnPath_MissingCommand(t *testing.T) {
|
|||||||
t.Errorf("expected error for missing command")
|
t.Errorf("expected error for missing command")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDoctorChecksFor_Web_OmitsJava(t *testing.T) {
|
||||||
|
for _, c := range doctorChecksFor("web") {
|
||||||
|
if strings.Contains(c.Name, "java") || strings.Contains(c.Name, "sidecar") || strings.Contains(c.Name, "adb") {
|
||||||
|
t.Errorf("web checks should not include %q", c.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(doctorChecksFor("web")) == 0 {
|
||||||
|
t.Error("web checks empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoctorChecksFor_Android_IncludesADB(t *testing.T) {
|
||||||
|
checks := doctorChecksFor("android")
|
||||||
|
found := false
|
||||||
|
for _, c := range checks {
|
||||||
|
if strings.Contains(c.Name, "adb") {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("android checks missing adb: %+v", checks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoctorChecksFor_iOS_IncludesXcrun(t *testing.T) {
|
||||||
|
checks := doctorChecksFor("ios")
|
||||||
|
found := false
|
||||||
|
for _, c := range checks {
|
||||||
|
if strings.Contains(c.Name, "xcrun") {
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("ios checks missing xcrun: %+v", checks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoctorChecksFor_All_IsUnion(t *testing.T) {
|
||||||
|
all := doctorChecksFor("all")
|
||||||
|
names := map[string]int{}
|
||||||
|
for _, c := range all {
|
||||||
|
names[c.Name]++
|
||||||
|
}
|
||||||
|
for _, name := range []string{"adb on PATH", "xcrun on PATH", "headless chromium can launch"} {
|
||||||
|
if names[name] != 1 {
|
||||||
|
t.Errorf("expected %q in 'all' exactly once, got %d", name, names[name])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDoctorChecksFor_UnknownPlatform(t *testing.T) {
|
||||||
|
if got := doctorChecksFor("fuchsia"); got != nil {
|
||||||
|
t.Errorf("expected nil for unknown platform, got %+v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDoctorArgs_DefaultAll(t *testing.T) {
|
||||||
|
options, err := parseDoctorArgs(nil, io.Discard)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if options.platform != "all" {
|
||||||
|
t.Errorf("default platform: got %q, want all", options.platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDoctorArgs_ExplicitPlatform(t *testing.T) {
|
||||||
|
for _, form := range [][]string{
|
||||||
|
{"--platform", "web"},
|
||||||
|
{"--platform=web"},
|
||||||
|
} {
|
||||||
|
options, err := parseDoctorArgs(form, io.Discard)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v: %v", form, err)
|
||||||
|
}
|
||||||
|
if options.platform != "web" {
|
||||||
|
t.Errorf("%v: got platform=%q, want web", form, options.platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDoctorArgs_RejectsUnknown(t *testing.T) {
|
||||||
|
if _, err := parseDoctorArgs([]string{"--platform=fuchsia"}, io.Discard); err == nil {
|
||||||
|
t.Error("expected error for unsupported platform")
|
||||||
|
}
|
||||||
|
if _, err := parseDoctorArgs([]string{"--bogus"}, io.Discard); err == nil {
|
||||||
|
t.Error("expected error for unknown argument")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDoctorArgs_HelpReturnsErrHelp(t *testing.T) {
|
||||||
|
if _, err := parseDoctorArgs([]string{"-h"}, io.Discard); !errors.Is(err, flag.ErrHelp) {
|
||||||
|
t.Errorf("expected flag.ErrHelp for -h, got %v", err)
|
||||||
|
}
|
||||||
|
if _, err := parseDoctorArgs([]string{"--help"}, io.Discard); !errors.Is(err, flag.ErrHelp) {
|
||||||
|
t.Errorf("expected flag.ErrHelp for --help, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-3
@@ -76,8 +76,13 @@ func runTest(options testOptions, stdout io.Writer) error {
|
|||||||
return runTestPipeline(ctx, options, stdout)
|
return runTestPipeline(ctx, options, stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runDoctor(stdout io.Writer) error {
|
func runDoctor(args []string, stdout, stderr io.Writer) error {
|
||||||
return runDoctorChecks(context.Background(), defaultDoctorChecks(), stdout)
|
options, err := parseDoctorArgs(args, stderr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
checks := doctorChecksFor(options.platform)
|
||||||
|
return runDoctorChecks(context.Background(), checks, stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(args []string, stdout, stderr io.Writer) error {
|
func run(args []string, stdout, stderr io.Writer) error {
|
||||||
@@ -99,7 +104,7 @@ func run(args []string, stdout, stderr io.Writer) error {
|
|||||||
}
|
}
|
||||||
return runInspect(options, stdout)
|
return runInspect(options, stdout)
|
||||||
case "doctor":
|
case "doctor":
|
||||||
return runDoctor(stdout)
|
return runDoctor(args[2:], stdout, stderr)
|
||||||
case "version", "-v", "--version":
|
case "version", "-v", "--version":
|
||||||
fmt.Fprintln(stdout, Version)
|
fmt.Fprintln(stdout, Version)
|
||||||
return nil
|
return nil
|
||||||
@@ -110,6 +115,11 @@ func run(args []string, stdout, stderr io.Writer) error {
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if err := run(os.Args, os.Stdout, os.Stderr); err != nil {
|
if err := run(os.Args, os.Stdout, os.Stderr); err != nil {
|
||||||
|
// flag.ErrHelp means -h/--help was requested; flag already printed
|
||||||
|
// usage to stderr, so exit 0 rather than treating it as a failure.
|
||||||
|
if errors.Is(err, flag.ErrHelp) {
|
||||||
|
return
|
||||||
|
}
|
||||||
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
sanderling/runs/
|
||||||
@@ -8,81 +8,79 @@ import {
|
|||||||
from,
|
from,
|
||||||
next,
|
next,
|
||||||
now,
|
now,
|
||||||
swipes,
|
|
||||||
taps,
|
taps,
|
||||||
waitOnce,
|
waitOnce,
|
||||||
weighted,
|
weighted,
|
||||||
} from "@sanderling/spec";
|
} from "@sanderling/spec";
|
||||||
import { noUncaughtExceptions } from "@sanderling/spec/defaults/properties";
|
import { noUncaughtExceptions } from "@sanderling/spec/defaults/properties";
|
||||||
|
|
||||||
// Route detection via stable DOM ids
|
// Page-presence checks via stable element ids.
|
||||||
const onLoginPage = extract((s) => !!s.ax.find("id:email"));
|
const onLoginPage = extract((s) => !!s.ax.find({ id: "email" }));
|
||||||
const onHomePage = extract((s) => !!s.ax.find("id:add-account"));
|
const onHomePage = extract((s) => !!s.ax.find({ id: "add-account" }));
|
||||||
const onAddAccountPage = extract((s) => !!s.ax.find("id:account-name"));
|
const onAddAccountPage = extract((s) => !!s.ax.find({ id: "account-name" }));
|
||||||
const onLedgerPage = extract((s) => !!s.ax.find("id:ledger"));
|
const onLedgerPage = extract((s) => !!s.ax.find({ id: "ledger" }));
|
||||||
const onAddTxnPage = extract((s) => !!s.ax.find("id:txn-amount"));
|
const onAddTxnPage = extract((s) => !!s.ax.find({ id: "txn-amount" }));
|
||||||
|
|
||||||
// Auth state: true on any authenticated page, false only on login page
|
// Auth state: true on any authenticated page, false only on login page.
|
||||||
const loggedIn = extract((s) => {
|
const loggedIn = extract((s) => {
|
||||||
if (s.ax.find("id:email")) return false;
|
if (s.ax.find({ id: "email" })) return false;
|
||||||
return !!(
|
return !!(
|
||||||
s.ax.find("id:logout") ||
|
s.ax.find({ id: "logout" }) ||
|
||||||
s.ax.find("id:add-account") ||
|
s.ax.find({ id: "add-account" }) ||
|
||||||
s.ax.find("id:ledger") ||
|
s.ax.find({ id: "ledger" }) ||
|
||||||
s.ax.find("id:account-name") ||
|
s.ax.find({ id: "account-name" }) ||
|
||||||
s.ax.find("id:txn-amount") ||
|
s.ax.find({ id: "txn-amount" }) ||
|
||||||
s.ax.find("id:add-txn")
|
s.ax.find({ id: "add-txn" })
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Total balance from title attribute on id:total-balance (set to raw cents integer)
|
// Read raw cents off explicit data-cents attributes; no aria-label parsing.
|
||||||
|
function readCents(value: string | undefined): number {
|
||||||
|
if (!value) return 0;
|
||||||
|
const parsed = parseInt(value, 10);
|
||||||
|
return isNaN(parsed) ? 0 : parsed;
|
||||||
|
}
|
||||||
|
|
||||||
const totalBalance = extract((s) => {
|
const totalBalance = extract((s) => {
|
||||||
const el = s.ax.find("id:total-balance");
|
const el = s.ax.find({ id: "total-balance" });
|
||||||
if (!el) return 0;
|
return readCents(el?.attrs?.["data-cents"]);
|
||||||
const v = parseInt((el as { desc?: string }).desc ?? "", 10);
|
|
||||||
return isNaN(v) ? 0 : v;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Account cards expose "account:{id}:{balance}" via aria-label -> content-desc
|
// Account cards expose `data-account-id` + `data-balance` so the spec reads
|
||||||
|
// structured data without parsing aria-label.
|
||||||
const accountCards = extract((s) => {
|
const accountCards = extract((s) => {
|
||||||
return s.ax.findAll("descPrefix:account:").map((el) => {
|
return s.ax.findAll({ "data-testid": "account-card" }).map((el) => ({
|
||||||
const parts = ((el as { desc?: string }).desc ?? "").split(":");
|
element: el,
|
||||||
const balance = parseInt(parts[2] ?? "0", 10);
|
id: el.attrs?.["data-account-id"] ?? "",
|
||||||
return { element: el, id: parts[1] ?? "", balance: isNaN(balance) ? 0 : balance };
|
balance: readCents(el.attrs?.["data-balance"]),
|
||||||
});
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ledger txn count from title on id:ledger
|
|
||||||
const ledgerTxnCount = extract((s) => {
|
const ledgerTxnCount = extract((s) => {
|
||||||
const el = s.ax.find("id:ledger");
|
const el = s.ax.find({ id: "ledger" });
|
||||||
if (!el) return 0;
|
return readCents(el?.attrs?.["data-txn-count"]);
|
||||||
const v = parseInt((el as { desc?: string }).desc ?? "", 10);
|
|
||||||
return isNaN(v) ? 0 : v;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Ledger balance from title on id:ledger-balance
|
|
||||||
const ledgerBalance = extract((s) => {
|
const ledgerBalance = extract((s) => {
|
||||||
const el = s.ax.find("id:ledger-balance");
|
const el = s.ax.find({ id: "ledger-balance" });
|
||||||
if (!el) return 0;
|
return readCents(el?.attrs?.["data-cents"]);
|
||||||
const v = parseInt((el as { desc?: string }).desc ?? "", 10);
|
|
||||||
return isNaN(v) ? 0 : v;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// UI element handles
|
// UI element handles.
|
||||||
const emailField = extract((s) => s.ax.find("id:email"));
|
const emailField = extract((s) => s.ax.find({ id: "email" }));
|
||||||
const passwordField = extract((s) => s.ax.find("id:password"));
|
const passwordField = extract((s) => s.ax.find({ id: "password" }));
|
||||||
const loginSubmit = extract((s) => s.ax.find("id:login-submit"));
|
const loginSubmit = extract((s) => s.ax.find({ id: "login-submit" }));
|
||||||
const logoutButton = extract((s) => s.ax.find("id:logout"));
|
const logoutButton = extract((s) => s.ax.find({ id: "logout" }));
|
||||||
const addAccountButton = extract((s) => s.ax.find("id:add-account"));
|
const addAccountButton = extract((s) => s.ax.find({ id: "add-account" }));
|
||||||
const accountNameField = extract((s) => s.ax.find("id:account-name"));
|
const accountNameField = extract((s) => s.ax.find({ id: "account-name" }));
|
||||||
const addAccountSubmit = extract((s) => s.ax.find("id:add-account-submit"));
|
const addAccountSubmit = extract((s) => s.ax.find({ id: "add-account-submit" }));
|
||||||
const addTxnButton = extract((s) => s.ax.find("id:add-txn"));
|
const addTxnButton = extract((s) => s.ax.find({ id: "add-txn" }));
|
||||||
const txnAmountField = extract((s) => s.ax.find("id:txn-amount"));
|
const txnAmountField = extract((s) => s.ax.find({ id: "txn-amount" }));
|
||||||
const txnNoteField = extract((s) => s.ax.find("id:txn-note"));
|
const txnNoteField = extract((s) => s.ax.find({ id: "txn-note" }));
|
||||||
const txnCreditButton = extract((s) => s.ax.find("id:txn-credit"));
|
const txnCreditButton = extract((s) => s.ax.find({ id: "txn-credit" }));
|
||||||
const txnDebitButton = extract((s) => s.ax.find("id:txn-debit"));
|
const txnDebitButton = extract((s) => s.ax.find({ id: "txn-debit" }));
|
||||||
const txnSubmit = extract((s) => s.ax.find("id:txn-submit"));
|
const txnSubmit = extract((s) => s.ax.find({ id: "txn-submit" }));
|
||||||
const backButton = extract((s) => s.ax.find("id:back"));
|
const backButton = extract((s) => s.ax.find({ id: "back" }));
|
||||||
|
|
||||||
// -- Properties --
|
// -- Properties --
|
||||||
|
|
||||||
@@ -98,8 +96,6 @@ const loggedOutReachesLogin = always(
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Total balance must equal the sum of all account balances visible on home page.
|
|
||||||
// Only checked when home page is active and account cards are loaded.
|
|
||||||
const totalBalanceMatchesAccounts = always(() => {
|
const totalBalanceMatchesAccounts = always(() => {
|
||||||
if (!onHomePage.current) return true;
|
if (!onHomePage.current) return true;
|
||||||
const cards = accountCards.current;
|
const cards = accountCards.current;
|
||||||
@@ -108,8 +104,6 @@ const totalBalanceMatchesAccounts = always(() => {
|
|||||||
return sum === totalBalance.current;
|
return sum === totalBalance.current;
|
||||||
});
|
});
|
||||||
|
|
||||||
// When a transaction is added (ledger txn count increases by 1 on the same
|
|
||||||
// account), the ledger balance delta matches what was actually stored.
|
|
||||||
const balanceMatchesTransactionDelta = always(
|
const balanceMatchesTransactionDelta = always(
|
||||||
now(() => onLedgerPage.current && ledgerTxnCount.current > 0).implies(
|
now(() => onLedgerPage.current && ledgerTxnCount.current > 0).implies(
|
||||||
next(() => {
|
next(() => {
|
||||||
@@ -119,7 +113,6 @@ const balanceMatchesTransactionDelta = always(
|
|||||||
if (curCount !== prevCount + 1) return true;
|
if (curCount !== prevCount + 1) return true;
|
||||||
const prevBal = ledgerBalance.previous ?? 0;
|
const prevBal = ledgerBalance.previous ?? 0;
|
||||||
const curBal = ledgerBalance.current;
|
const curBal = ledgerBalance.current;
|
||||||
// Delta must be non-zero (a valid transaction was added)
|
|
||||||
return curBal !== prevBal;
|
return curBal !== prevBal;
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
@@ -149,19 +142,7 @@ export const properties = {
|
|||||||
const DEMO_EMAIL = "[email protected]";
|
const DEMO_EMAIL = "[email protected]";
|
||||||
const DEMO_PASSWORD = "ledger123";
|
const DEMO_PASSWORD = "ledger123";
|
||||||
|
|
||||||
const loginHelper = actions(() => {
|
function focusedField(): string | null {
|
||||||
if (loggedIn.current) return [];
|
|
||||||
const email = emailField.current;
|
|
||||||
const password = passwordField.current;
|
|
||||||
const submit = loginSubmit.current;
|
|
||||||
if (!email || !password || !submit) return [];
|
|
||||||
const focused = s_focused();
|
|
||||||
if (focused === "password") return [Tap({ on: submit })];
|
|
||||||
if (focused === "email") return [InputText({ into: password, text: DEMO_PASSWORD })];
|
|
||||||
return [InputText({ into: email, text: DEMO_EMAIL })];
|
|
||||||
});
|
|
||||||
|
|
||||||
function s_focused(): string | null {
|
|
||||||
const email = emailField.current;
|
const email = emailField.current;
|
||||||
const password = passwordField.current;
|
const password = passwordField.current;
|
||||||
if (email && (email as { focused?: boolean }).focused) return "email";
|
if (email && (email as { focused?: boolean }).focused) return "email";
|
||||||
@@ -169,6 +150,18 @@ function s_focused(): string | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loginHelper = actions(() => {
|
||||||
|
if (loggedIn.current) return [];
|
||||||
|
const email = emailField.current;
|
||||||
|
const password = passwordField.current;
|
||||||
|
const submit = loginSubmit.current;
|
||||||
|
if (!email || !password || !submit) return [];
|
||||||
|
const focused = focusedField();
|
||||||
|
if (focused === "password") return [Tap({ on: submit })];
|
||||||
|
if (focused === "email") return [InputText({ into: password, text: DEMO_PASSWORD })];
|
||||||
|
return [InputText({ into: email, text: DEMO_EMAIL })];
|
||||||
|
});
|
||||||
|
|
||||||
const adversarialLogin = actions(() => {
|
const adversarialLogin = actions(() => {
|
||||||
if (loggedIn.current) return [];
|
if (loggedIn.current) return [];
|
||||||
const submit = loginSubmit.current;
|
const submit = loginSubmit.current;
|
||||||
@@ -297,7 +290,6 @@ export const actionsRoot = weighted(
|
|||||||
[6, goBack],
|
[6, goBack],
|
||||||
[1, logoutAction],
|
[1, logoutAction],
|
||||||
[4, taps],
|
[4, taps],
|
||||||
[2, swipes],
|
|
||||||
[2, waitOnce],
|
[2, waitOnce],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -86,8 +86,7 @@ export function HomePage(props: {
|
|||||||
<div
|
<div
|
||||||
id="total-balance"
|
id="total-balance"
|
||||||
className="balance-display"
|
className="balance-display"
|
||||||
data-value={totalBalance}
|
data-cents={totalBalance}
|
||||||
title={String(totalBalance)}
|
|
||||||
>
|
>
|
||||||
<span className="balance-label">Total balance</span>
|
<span className="balance-label">Total balance</span>
|
||||||
<span
|
<span
|
||||||
@@ -164,7 +163,7 @@ export function HomePage(props: {
|
|||||||
data-name={a.name}
|
data-name={a.name}
|
||||||
data-balance={bal}
|
data-balance={bal}
|
||||||
data-txn-count={countTxns(txns, a.id)}
|
data-txn-count={countTxns(txns, a.id)}
|
||||||
aria-label={'account:' + a.id + ':' + bal}
|
aria-label={`${a.name}, ${formatCents(bal)}`}
|
||||||
onClick={() => navigate(`/accounts/${a.id}`)}
|
onClick={() => navigate(`/accounts/${a.id}`)}
|
||||||
>
|
>
|
||||||
<span className="account-avatar" aria-hidden="true">
|
<span className="account-avatar" aria-hidden="true">
|
||||||
|
|||||||
@@ -93,13 +93,11 @@ export function LedgerPage(props: {
|
|||||||
data-account-id={props.accountId}
|
data-account-id={props.accountId}
|
||||||
data-account-name={account?.name ?? ''}
|
data-account-name={account?.name ?? ''}
|
||||||
data-txn-count={txns?.length ?? 0}
|
data-txn-count={txns?.length ?? 0}
|
||||||
title={String(txns?.length ?? 0)}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="ledger-balance"
|
id="ledger-balance"
|
||||||
className="balance-display"
|
className="balance-display"
|
||||||
data-value={balance}
|
data-cents={balance}
|
||||||
title={String(balance)}
|
|
||||||
>
|
>
|
||||||
<span className="balance-label">Balance</span>
|
<span className="balance-label">Balance</span>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"react-router-dom": "^6.26.2",
|
"react-router-dom": "^6.26.2",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.3.13",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
@@ -175,8 +176,12 @@
|
|||||||
|
|
||||||
"@types/babel__traverse": ["@types/[email protected]", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
"@types/babel__traverse": ["@types/[email protected]", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
|
||||||
|
|
||||||
|
"@types/bun": ["@types/[email protected]", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
|
||||||
|
|
||||||
"@types/estree": ["@types/[email protected]", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
"@types/estree": ["@types/[email protected]", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/[email protected]", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="],
|
||||||
|
|
||||||
"@types/prop-types": ["@types/[email protected]", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
|
"@types/prop-types": ["@types/[email protected]", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/[email protected]", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="],
|
"@types/react": ["@types/[email protected]", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" } }, "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw=="],
|
||||||
@@ -189,6 +194,8 @@
|
|||||||
|
|
||||||
"browserslist": ["[email protected]", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
"browserslist": ["[email protected]", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||||
|
|
||||||
|
"bun-types": ["[email protected]", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
|
||||||
|
|
||||||
"caniuse-lite": ["[email protected]", "", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="],
|
"caniuse-lite": ["[email protected]", "", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="],
|
||||||
|
|
||||||
"convert-source-map": ["[email protected]", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
"convert-source-map": ["[email protected]", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
@@ -247,6 +254,8 @@
|
|||||||
|
|
||||||
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
"typescript": ["[email protected]", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||||
|
|
||||||
|
"undici-types": ["[email protected]", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
|
||||||
|
|
||||||
"update-browserslist-db": ["[email protected]", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
"update-browserslist-db": ["[email protected]", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="],
|
||||||
|
|
||||||
"vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
|
"vite": ["[email protected]", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
|
||||||
|
|||||||
-1
@@ -1 +0,0 @@
|
|||||||
@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/JetBrainsMono-Regular.woff2) format("woff2")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/JetBrainsMono-Medium.woff2) format("woff2")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/JetBrainsMono-Bold.woff2) format("woff2")}body{font-family:var(--font-mono);font-size:13px;line-height:1.5;color:var(--text-primary);background:var(--background);margin:0}:root{--background: #ffffff;--surface: #f7f7f7;--surface-elevated: #ffffff;--border: #e4e4e4;--border-strong: #c8c8c8;--text-primary: #111111;--text-muted: #6b6b6b;--accent-violation: #d32f2f;--accent-positive: #2e7d32;--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace}:root[data-theme=dark],:root[data-theme=dark] body{--background: #0e0e0e;--surface: #161616;--surface-elevated: #1d1d1d;--border: #2a2a2a;--border-strong: #3a3a3a;--text-primary: #f0f0f0;--text-muted: #9a9a9a;--accent-violation: #f06b6b;--accent-positive: #7cc47c}@media (prefers-color-scheme: dark){:root:not([data-theme=light]){--background: #0e0e0e;--surface: #161616;--surface-elevated: #1d1d1d;--border: #2a2a2a;--border-strong: #3a3a3a;--text-primary: #f0f0f0;--text-muted: #9a9a9a;--accent-violation: #f06b6b;--accent-positive: #7cc47c}}body{background:var(--background);color:var(--text-primary);margin:0;font-family:var(--font-mono)}*,*:before,*:after{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{overflow:hidden}a{color:inherit;text-decoration:none}a:hover{text-decoration:underline}button{font:inherit;color:inherit;background:var(--surface);border:1px solid var(--border);padding:4px 10px;cursor:pointer}button:hover{border-color:var(--border-strong)}.app-shell{height:100vh;display:flex;flex-direction:column;overflow:hidden}.app-main{flex:1 1 auto;min-height:0;padding:0;display:flex;flex-direction:column}.app-main>*{flex:1 1 auto;min-height:0}.run-table{width:100%;border-collapse:collapse;align-self:flex-start;flex:0 0 auto}.run-table th,.run-table td{text-align:left;padding:8px 12px;border-bottom:1px solid var(--border)}.run-table th{color:var(--text-muted);font-weight:500;font-size:12px;text-transform:uppercase;letter-spacing:.04em}.run-table tr:hover td{background:var(--surface)}.chip{display:inline-block;padding:2px 8px;border:1px solid var(--border-strong);border-radius:999px;font-size:11px}.chip-violation{border-color:var(--accent-violation);color:var(--accent-violation)}.chip-progress{border-color:var(--border-strong);color:var(--text-muted)}.detail-root{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}.detail-toolbar{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;border-bottom:1px solid var(--border);flex:0 0 auto}.detail-toolbar-meta{display:flex;gap:16px;align-items:baseline;font-size:12px;color:var(--text-muted)}.detail-toolbar-meta strong{color:var(--text-primary);font-weight:500}.theme-toggle{display:inline-flex;align-items:center;justify-content:center;width:28px;height:24px;padding:0;color:var(--text-muted)}.theme-toggle:hover{color:var(--text-primary)}.theme-toggle svg{display:block}.detail-grid{flex:1 1 auto;display:grid;grid-template-columns:340px minmax(0,1fr) minmax(0,1fr);grid-template-rows:minmax(0,1fr) minmax(0,130px);grid-template-areas:"actions before after" "metrics metrics metrics";gap:0;padding:0;min-height:0;overflow:hidden}.detail-grid>.detail-actions{grid-area:actions;min-height:0;overflow:hidden}.detail-grid>.detail-state-before,.detail-grid>.detail-state-after{display:flex;flex-direction:column;gap:0;min-height:0;min-width:0;overflow:hidden}.detail-grid>.detail-state-before{grid-area:before}.detail-grid>.detail-state-after{grid-area:after}.detail-grid>.detail-actions,.detail-grid>.detail-state-before{border-right:1px solid var(--border)}.detail-grid>.detail-state-before .screenshot-frame,.detail-grid>.detail-state-after .screenshot-frame,.detail-grid>.detail-state-before .screenshot-placeholder,.detail-grid>.detail-state-after .screenshot-placeholder{flex:1 1 auto;max-height:100%;max-width:100%;margin:0 auto;min-height:0}.detail-grid>.detail-metrics{grid-area:metrics;overflow:hidden;display:flex;flex-direction:column;min-height:0;border-top:1px solid var(--border)}.detail-grid>.detail-metrics .metrics-chart{flex:1 1 auto;min-height:0;max-height:100%}.detail-metrics .detail-panel-body{overflow:hidden}.detail-metrics-exceptions{padding:4px 8px;border-top:1px solid var(--border);overflow:auto;max-height:80px}.detail-panel{border:none;background:transparent;padding:0;min-height:0;display:flex;flex-direction:column}.detail-panel h2{margin:0;padding:4px 8px;font-size:10px;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--border);flex:0 0 auto}.detail-panel-body{flex:1 1 auto;mLine truncated
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:400;font-display:swap;src:url(/fonts/JetBrainsMono-Regular.woff2) format("woff2")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:500;font-display:swap;src:url(/fonts/JetBrainsMono-Medium.woff2) format("woff2")}@font-face{font-family:JetBrains Mono;font-style:normal;font-weight:700;font-display:swap;src:url(/fonts/JetBrainsMono-Bold.woff2) format("woff2")}body{font-family:var(--font-mono);font-size:13px;line-height:1.5;color:var(--text-primary);background:var(--background);margin:0}:root{--background: #ffffff;--surface: #f7f7f7;--surface-elevated: #ffffff;--border: #e4e4e4;--border-strong: #c8c8c8;--text-primary: #111111;--text-muted: #6b6b6b;--accent-violation: #d32f2f;--accent-positive: #2e7d32;--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace}:root[data-theme=dark],:root[data-theme=dark] body{--background: #0e0e0e;--surface: #161616;--surface-elevated: #1d1d1d;--border: #2a2a2a;--border-strong: #3a3a3a;--text-primary: #f0f0f0;--text-muted: #9a9a9a;--accent-violation: #f06b6b;--accent-positive: #7cc47c}@media (prefers-color-scheme: dark){:root:not([data-theme=light]){--background: #0e0e0e;--surface: #161616;--surface-elevated: #1d1d1d;--border: #2a2a2a;--border-strong: #3a3a3a;--text-primary: #f0f0f0;--text-muted: #9a9a9a;--accent-violation: #f06b6b;--accent-positive: #7cc47c}}body{background:var(--background);color:var(--text-primary);margin:0;font-family:var(--font-mono)}*,*:before,*:after{box-sizing:border-box}html,body,#root{height:100%;margin:0}body{overflow:hidden}a{color:inherit;text-decoration:none}a:hover{text-decoration:underline}button{font:inherit;color:inherit;background:var(--surface);border:1px solid var(--border);padding:4px 10px;cursor:pointer}button:hover{border-color:var(--border-strong)}.app-shell{height:100vh;display:flex;flex-direction:column;overflow:hidden}.app-main{flex:1 1 auto;min-height:0;padding:0;display:flex;flex-direction:column}.app-main>*{flex:1 1 auto;min-height:0}.run-table{width:100%;border-collapse:collapse;align-self:flex-start;flex:0 0 auto}.run-table th,.run-table td{text-align:left;padding:8px 12px;border-bottom:1px solid var(--border)}.run-table th{color:var(--text-muted);font-weight:500;font-size:12px;text-transform:uppercase;letter-spacing:.04em}.run-table tr:hover td{background:var(--surface)}.chip{display:inline-block;padding:2px 8px;border:1px solid var(--border-strong);border-radius:999px;font-size:11px}.chip-violation{border-color:var(--accent-violation);color:var(--accent-violation)}.chip-progress{border-color:var(--border-strong);color:var(--text-muted)}.detail-root{display:flex;flex-direction:column;height:100%;min-height:0;overflow:hidden}.detail-toolbar{display:flex;align-items:center;justify-content:space-between;padding:4px 8px;border-bottom:1px solid var(--border);flex:0 0 auto}.detail-toolbar-meta{display:flex;gap:16px;align-items:baseline;font-size:12px;color:var(--text-muted)}.detail-toolbar-meta strong{color:var(--text-primary);font-weight:500}.theme-toggle{display:inline-flex;align-items:center;justify-content:center;width:28px;height:24px;padding:0;color:var(--text-muted)}.theme-toggle:hover{color:var(--text-primary)}.theme-toggle svg{display:block}.detail-grid{flex:1 1 auto;display:grid;grid-template-columns:340px minmax(0,1fr) minmax(0,1fr);grid-template-rows:minmax(0,1fr) minmax(0,130px);grid-template-areas:"actions before after" "metrics metrics metrics";gap:0;padding:0;min-height:0;overflow:hidden}.detail-grid>.detail-actions{grid-area:actions;min-height:0;overflow:hidden}.detail-grid>.detail-state-before,.detail-grid>.detail-state-after{display:flex;flex-direction:column;gap:0;min-height:0;min-width:0;overflow:hidden}.detail-grid>.detail-state-before{grid-area:before}.detail-grid>.detail-state-after{grid-area:after}.detail-grid>.detail-actions,.detail-grid>.detail-state-before{border-right:1px solid var(--border)}.detail-grid>.detail-state-before .screenshot-frame,.detail-grid>.detail-state-after .screenshot-frame,.detail-grid>.detail-state-before .screenshot-placeholder,.detail-grid>.detail-state-after .screenshot-placeholder{flex:1 1 auto;max-height:100%;max-width:100%;margin:0 auto;min-height:0}.detail-grid>.detail-metrics{grid-area:metrics;overflow:hidden;display:flex;flex-direction:column;min-height:0;border-top:1px solid var(--border)}.detail-grid>.detail-metrics .metrics-chart{flex:1 1 auto;min-height:0;max-height:100%}.detail-metrics .detail-panel-body{overflow:hidden}.detail-metrics-exceptions{padding:4px 8px;border-top:1px solid var(--border);overflow:auto;max-height:80px}.detail-panel{border:none;background:transparent;padding:0;min-height:0;display:flex;flex-direction:column}.detail-panel h2{margin:0;padding:4px 8px;font-size:10px;font-weight:500;color:var(--text-muted);text-transform:uppercase;letter-spacing:.06em;border-bottom:1px solid var(--border);flex:0 0 auto}.detail-panel-body{flex:1 1 auto;mLine truncated
|
||||||
+11
-11
@@ -1,4 +1,4 @@
|
|||||||
function xf(e,t){for(var n=0;n<t.length;n++){const r=t[n];if(typeof r!="string"&&!Array.isArray(r)){for(const l in r)if(l!=="default"&&!(l in e)){const i=Object.getOwnPropertyDescriptor(r,l);i&&Object.defineProperty(e,l,i.get?i:{enumerable:!0,get:()=>r[l]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();function wf(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Au={exports:{}},yl={},Vu={exports:{}},O={};/**
|
function wf(e,t){for(var n=0;n<t.length;n++){const r=t[n];if(typeof r!="string"&&!Array.isArray(r)){for(const l in r)if(l!=="default"&&!(l in e)){const i=Object.getOwnPropertyDescriptor(r,l);i&&Object.defineProperty(e,l,i.get?i:{enumerable:!0,get:()=>r[l]})}}}return Object.freeze(Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}))}(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const i of l)if(i.type==="childList")for(const o of i.addedNodes)o.tagName==="LINK"&&o.rel==="modulepreload"&&r(o)}).observe(document,{childList:!0,subtree:!0});function n(l){const i={};return l.integrity&&(i.integrity=l.integrity),l.referrerPolicy&&(i.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?i.credentials="include":l.crossOrigin==="anonymous"?i.credentials="omit":i.credentials="same-origin",i}function r(l){if(l.ep)return;l.ep=!0;const i=n(l);fetch(l.href,i)}})();function Sf(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var Vu={exports:{}},yl={},Bu={exports:{}},O={};/**
|
||||||
* @license React
|
* @license React
|
||||||
* react.production.min.js
|
* react.production.min.js
|
||||||
*
|
*
|
||||||
@@ -6,7 +6,7 @@ function xf(e,t){for(var n=0;n<t.length;n++){const r=t[n];if(typeof r!="string"&
|
|||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/var sr=Symbol.for("react.element"),Sf=Symbol.for("react.portal"),kf=Symbol.for("react.fragment"),Ef=Symbol.for("react.strict_mode"),Cf=Symbol.for("react.profiler"),Nf=Symbol.for("react.provider"),jf=Symbol.for("react.context"),_f=Symbol.for("react.forward_ref"),Pf=Symbol.for("react.suspense"),Tf=Symbol.for("react.memo"),Lf=Symbol.for("react.lazy"),cs=Symbol.iterator;function Rf(e){return e===null||typeof e!="object"?null:(e=cs&&e[cs]||e["@@iterator"],typeof e=="function"?e:null)}var Bu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Wu=Object.assign,Hu={};function mn(e,t,n){this.props=e,this.context=t,this.refs=Hu,this.updater=n||Bu}mn.prototype.isReactComponent={};mn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};mn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Qu(){}Qu.prototype=mn.prototype;function uo(e,t,n){this.props=e,this.context=t,this.refs=Hu,this.updater=n||Bu}var ao=uo.prototype=new Qu;ao.constructor=uo;Wu(ao,mn.prototype);ao.isPureReactComponent=!0;var fs=Array.isArray,Ku=Object.prototype.hasOwnProperty,co={current:null},Gu={key:!0,ref:!0,__self:!0,__source:!0};function Ju(e,t,n){var r,l={},i=null,o=null;if(t!=null)for(r in t.ref!==void 0&&(o=t.ref),t.key!==void 0&&(i=""+t.key),t)Ku.call(t,r)&&!Gu.hasOwnProperty(r)&&(l[r]=t[r]);var s=arguments.length-2;if(s===1)l.children=n;else if(1<s){for(var u=Array(s),a=0;a<s;a++)u[a]=arguments[a+2];l.children=u}if(e&&e.defaultProps)for(r in s=e.defaultProps,s)l[r]===void 0&&(l[r]=s[r]);return{$$typeof:sr,type:e,key:i,ref:o,props:l,_owner:co.current}}function zf(e,t){return{$$typeof:sr,type:e.type,key:t,ref:e.ref,props:e.props,_owner:e._owner}}function fo(e){return typeof e=="object"&&e!==null&&e.$$typeof===sr}function If(e){var t={"=":"=0",":":"=2"};return"$"+e.replace(/[=:]/g,function(n){return t[n]})}var ds=/\/+/g;function $l(e,t){return typeof e=="object"&&e!==null&&e.key!=null?If(""+e.key):t.toString(36)}function Mr(e,t,n,r,l){var i=typeof e;(i==="undefined"||i==="boolean")&&(e=null);var o=!1;if(e===null)o=!0;else switch(i){case"string":case"number":o=!0;break;case"object":switch(e.$$typeof){case sr:case Sf:o=!0}}if(o)return o=e,l=l(o),e=r===""?"."+$l(o,0):r,fs(l)?(n="",e!=null&&(n=e.replace(ds,"$&/")+"/"),Mr(l,t,n,"",function(a){return a})):l!=null&&(fo(l)&&(l=zf(l,n+(!l.key||o&&o.key===l.key?"":(""+l.key).replace(ds,"$&/")+"/")+e)),t.push(l)),1;if(o=0,r=r===""?".":r+":",fs(e))for(var s=0;s<e.length;s++){i=e[s];var u=r+$l(i,s);o+=Mr(i,t,n,u,l)}else if(u=Rf(e),typeof u=="function")for(e=u.call(e),s=0;!(i=e.next()).done;)i=i.value,u=r+$l(i,s++),o+=Mr(i,t,n,u,l);else if(i==="object")throw t=String(e),Error("Objects are not valid as a React child (found: "+(t==="[object Object]"?"object with keys {"+Object.keys(e).join(", ")+"}":t)+"). If you meant to render a collection of children, use an array instead.");return o}function vr(e,t,n){if(e==null)return e;var r=[],l=0;return Mr(e,r,"","",function(i){return t.call(n,i,l++)}),r}function Of(e){if(e._status===-1){var t=e._result;t=t(),t.then(function(n){(e._status===0||e._status===-1)&&(e._status=1,e._result=n)},function(n){(e._status===0||e._status===-1)&&(e._status=2,e._result=n)}),e._status===-1&&(e._status=0,e._result=t)}if(e._status===1)return e._result.default;throw e._result}var de={current:null},Fr={transition:null},Mf={ReactCurrentDispatcher:de,ReactCurrentBatchConfig:Fr,ReactCurrentOwner:co};function Yu(){throw Error("act(...) is not supported in production builds of React.")}O.Children={map:vr,forEach:function(e,t,n){vr(e,function(){t.apply(this,arguments)},n)},count:function(e){var t=0;return vr(e,function(){t++}),t},toArray:function(e){return vr(e,function(t){return t})||[]},only:function(e){if(!fo(e))throw Error("React.Children.only expected to receive a single React element child.");return e}};O.Component=mn;O.Fragment=kf;O.Profiler=Cf;O.PureComponent=uo;O.StrictMode=Ef;O.Suspense=Pf;O.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=Mf;O.act=Yu;O.cloneElement=function(e,t,n){if(e==null)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+e+".");var r=Wu({},e.props),l=e.key,i=e.ref,o=e._owner;if(t!=null){if(t.ref!==void 0&&(i=t.ref,o=co.current),t.key!==void 0&&(l=""+t.key),e.type&&e.type.defaultProps)var s=e.type.defaultProps;for(u in t)Ku.call(t,u)&&!Gu.hasOwnProperty(u)&&(r[u]=t[u]===void 0&&s!==void 0?s[u]:t[u])}var u=arguments.length-2;if(u===1)r.children=n;else if(1<u){s=Array(u);for(var a=0;a<u;a++)s[a]=arguments[a+2];r.children=s}return{$$typeof:sr,type:e.type,key:l,ref:i,props:r,_owner:o}};O.createContext=function(e){return e={$$typeof:jf,_currentValue:e,_currentValue2:e,_threadCount:0,ProviderLine truncated
|
*/var sr=Symbol.for("react.element"),kf=Symbol.for("react.portal"),Ef=Symbol.for("react.fragment"),Nf=Symbol.for("react.strict_mode"),Cf=Symbol.for("react.profiler"),jf=Symbol.for("react.provider"),_f=Symbol.for("react.context"),Pf=Symbol.for("react.forward_ref"),Tf=Symbol.for("react.suspense"),Lf=Symbol.for("react.memo"),Rf=Symbol.for("react.lazy"),cs=Symbol.iterator;function zf(e){return e===null||typeof e!="object"?null:(e=cs&&e[cs]||e["@@iterator"],typeof e=="function"?e:null)}var Wu={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},Hu=Object.assign,Qu={};function mn(e,t,n){this.props=e,this.context=t,this.refs=Qu,this.updater=n||Wu}mn.prototype.isReactComponent={};mn.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};mn.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function Ku(){}Ku.prototype=mn.prototype;function uo(e,t,n){this.props=e,this.context=t,this.refs=Qu,this.updater=n||Wu}var ao=uo.prototype=new Ku;ao.constructor=uo;Hu(ao,mn.prototype);ao.isPureReactComponent=!0;var fs=Array.isArray,Gu=Object.prototype.hasOwnProperty,co={current:null},Ju={key:!0,ref:!0,__self:!0,__source:!0};function Yu(e,t,n){var r,l={},i=null,o=null;if(t!=null)for(r in t.ref!==void 0&&(o=t.ref),t.key!==void 0&&(i=""+t.key),t)Gu.call(t,r)&&!Ju.hasOwnProperty(r)&&(l[r]=t[r]);var s=arguments.length-2;if(s===1)l.children=n;else if(1<s){for(var u=Array(s),a=0;a<s;a++)u[a]=arguments[a+2];l.children=u}if(e&&e.defaultProps)for(r in s=e.defaultProps,s)l[r]===void 0&&(l[r]=s[r]);return{$$typeof:sr,type:e,key:i,ref:o,props:l,_owner:co.current}}function If(e,t){return{$$typeof:sr,type:e.type,key:t,ref:e.ref,props:e.props,_owner:e._owner}}function fo(e){return typeof e=="object"&&e!==null&&e.$$typeof===sr}function Of(e){var t={"=":"=0",":":"=2"};return"$"+e.replace(/[=:]/g,function(n){return t[n]})}var ds=/\/+/g;function $l(e,t){return typeof e=="object"&&e!==null&&e.key!=null?Of(""+e.key):t.toString(36)}function Mr(e,t,n,r,l){var i=typeof e;(i==="undefined"||i==="boolean")&&(e=null);var o=!1;if(e===null)o=!0;else switch(i){case"string":case"number":o=!0;break;case"object":switch(e.$$typeof){case sr:case kf:o=!0}}if(o)return o=e,l=l(o),e=r===""?"."+$l(o,0):r,fs(l)?(n="",e!=null&&(n=e.replace(ds,"$&/")+"/"),Mr(l,t,n,"",function(a){return a})):l!=null&&(fo(l)&&(l=If(l,n+(!l.key||o&&o.key===l.key?"":(""+l.key).replace(ds,"$&/")+"/")+e)),t.push(l)),1;if(o=0,r=r===""?".":r+":",fs(e))for(var s=0;s<e.length;s++){i=e[s];var u=r+$l(i,s);o+=Mr(i,t,n,u,l)}else if(u=zf(e),typeof u=="function")for(e=u.call(e),s=0;!(i=e.next()).done;)i=i.value,u=r+$l(i,s++),o+=Mr(i,t,n,u,l);else if(i==="object")throw t=String(e),Error("Objects are not valid as a React child (found: "+(t==="[object Object]"?"object with keys {"+Object.keys(e).join(", ")+"}":t)+"). If you meant to render a collection of children, use an array instead.");return o}function vr(e,t,n){if(e==null)return e;var r=[],l=0;return Mr(e,r,"","",function(i){return t.call(n,i,l++)}),r}function Mf(e){if(e._status===-1){var t=e._result;t=t(),t.then(function(n){(e._status===0||e._status===-1)&&(e._status=1,e._result=n)},function(n){(e._status===0||e._status===-1)&&(e._status=2,e._result=n)}),e._status===-1&&(e._status=0,e._result=t)}if(e._status===1)return e._result.default;throw e._result}var de={current:null},Fr={transition:null},Ff={ReactCurrentDispatcher:de,ReactCurrentBatchConfig:Fr,ReactCurrentOwner:co};function Xu(){throw Error("act(...) is not supported in production builds of React.")}O.Children={map:vr,forEach:function(e,t,n){vr(e,function(){t.apply(this,arguments)},n)},count:function(e){var t=0;return vr(e,function(){t++}),t},toArray:function(e){return vr(e,function(t){return t})||[]},only:function(e){if(!fo(e))throw Error("React.Children.only expected to receive a single React element child.");return e}};O.Component=mn;O.Fragment=Ef;O.Profiler=Cf;O.PureComponent=uo;O.StrictMode=Nf;O.Suspense=Tf;O.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=Ff;O.act=Xu;O.cloneElement=function(e,t,n){if(e==null)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+e+".");var r=Hu({},e.props),l=e.key,i=e.ref,o=e._owner;if(t!=null){if(t.ref!==void 0&&(i=t.ref,o=co.current),t.key!==void 0&&(l=""+t.key),e.type&&e.type.defaultProps)var s=e.type.defaultProps;for(u in t)Gu.call(t,u)&&!Ju.hasOwnProperty(u)&&(r[u]=t[u]===void 0&&s!==void 0?s[u]:t[u])}var u=arguments.length-2;if(u===1)r.children=n;else if(1<u){s=Array(u);for(var a=0;a<u;a++)s[a]=arguments[a+2];r.children=s}return{$$typeof:sr,type:e.type,key:l,ref:i,props:r,_owner:o}};O.createContext=function(e){return e={$$typeof:_f,_currentValue:e,_currentValue2:e,_threadCount:0,ProviderLine truncated
|
||||||
* @license React
|
* @license React
|
||||||
* react-jsx-runtime.production.min.js
|
* react-jsx-runtime.production.min.js
|
||||||
*
|
*
|
||||||
@@ -14,7 +14,7 @@ function xf(e,t){for(var n=0;n<t.length;n++){const r=t[n];if(typeof r!="string"&
|
|||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/var Df=S,$f=Symbol.for("react.element"),Uf=Symbol.for("react.fragment"),Af=Object.prototype.hasOwnProperty,Vf=Df.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,Bf={key:!0,ref:!0,__self:!0,__source:!0};function Zu(e,t,n){var r,l={},i=null,o=null;n!==void 0&&(i=""+n),t.key!==void 0&&(i=""+t.key),t.ref!==void 0&&(o=t.ref);for(r in t)Af.call(t,r)&&!Bf.hasOwnProperty(r)&&(l[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps,t)l[r]===void 0&&(l[r]=t[r]);return{$$typeof:$f,type:e,key:i,ref:o,props:l,_owner:Vf.current}}yl.Fragment=Uf;yl.jsx=Zu;yl.jsxs=Zu;Au.exports=yl;var p=Au.exports,qu={exports:{}},Ee={},bu={exports:{}},ea={};/**
|
*/var $f=S,Uf=Symbol.for("react.element"),Af=Symbol.for("react.fragment"),Vf=Object.prototype.hasOwnProperty,Bf=$f.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,Wf={key:!0,ref:!0,__self:!0,__source:!0};function qu(e,t,n){var r,l={},i=null,o=null;n!==void 0&&(i=""+n),t.key!==void 0&&(i=""+t.key),t.ref!==void 0&&(o=t.ref);for(r in t)Vf.call(t,r)&&!Wf.hasOwnProperty(r)&&(l[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps,t)l[r]===void 0&&(l[r]=t[r]);return{$$typeof:Uf,type:e,key:i,ref:o,props:l,_owner:Bf.current}}yl.Fragment=Af;yl.jsx=qu;yl.jsxs=qu;Vu.exports=yl;var c=Vu.exports,bu={exports:{}},Ee={},ea={exports:{}},ta={};/**
|
||||||
* @license React
|
* @license React
|
||||||
* scheduler.production.min.js
|
* scheduler.production.min.js
|
||||||
*
|
*
|
||||||
@@ -22,7 +22,7 @@ function xf(e,t){for(var n=0;n<t.length;n++){const r=t[n];if(typeof r!="string"&
|
|||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/(function(e){function t(T,z){var I=T.length;T.push(z);e:for(;0<I;){var K=I-1>>>1,ee=T[K];if(0<l(ee,z))T[K]=z,T[I]=ee,I=K;else break e}}function n(T){return T.length===0?null:T[0]}function r(T){if(T.length===0)return null;var z=T[0],I=T.pop();if(I!==z){T[0]=I;e:for(var K=0,ee=T.length,hr=ee>>>1;K<hr;){var Et=2*(K+1)-1,Dl=T[Et],Ct=Et+1,mr=T[Ct];if(0>l(Dl,I))Ct<ee&&0>l(mr,Dl)?(T[K]=mr,T[Ct]=I,K=Ct):(T[K]=Dl,T[Et]=I,K=Et);else if(Ct<ee&&0>l(mr,I))T[K]=mr,T[Ct]=I,K=Ct;else break e}}return z}function l(T,z){var I=T.sortIndex-z.sortIndex;return I!==0?I:T.id-z.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var u=[],a=[],h=1,c=null,d=3,y=!1,g=!1,w=!1,N=typeof setTimeout=="function"?setTimeout:null,m=typeof clearTimeout=="function"?clearTimeout:null,f=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function v(T){for(var z=n(a);z!==null;){if(z.callback===null)r(a);else if(z.startTime<=T)r(a),z.sortIndex=z.expirationTime,t(u,z);else break;z=n(a)}}function x(T){if(w=!1,v(T),!g)if(n(u)!==null)g=!0,Ml(E);else{var z=n(a);z!==null&&Fl(x,z.startTime-T)}}function E(T,z){g=!1,w&&(w=!1,m(C),C=-1),y=!0;var I=d;try{for(v(z),c=n(u);c!==null&&(!(c.expirationTime>z)||T&&!X());){var K=c.callback;if(typeof K=="function"){c.callback=null,d=c.priorityLevel;var ee=K(c.expirationTime<=z);z=e.unstable_now(),typeof ee=="function"?c.callback=ee:c===n(u)&&r(u),v(z)}else r(u);c=n(u)}if(c!==null)var hr=!0;else{var Et=n(a);Et!==null&&Fl(x,Et.startTime-z),hr=!1}return hr}finally{c=null,d=I,y=!1}}var _=!1,P=null,C=-1,R=5,L=-1;function X(){return!(e.unstable_now()-L<R)}function Q(){if(P!==null){var T=e.unstable_now();L=T;var z=!0;try{z=P(!0,T)}finally{z?b():(_=!1,P=null)}}else _=!1}var b;if(typeof f=="function")b=function(){f(Q)};else if(typeof MessageChannel<"u"){var pr=new MessageChannel,yf=pr.port2;pr.port1.onmessage=Q,b=function(){yf.postMessage(null)}}else b=function(){N(Q,0)};function Ml(T){P=T,_||(_=!0,b())}function Fl(T,z){C=N(function(){T(e.unstable_now())},z)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(T){T.callback=null},e.unstable_continueExecution=function(){g||y||(g=!0,Ml(E))},e.unstable_forceFrameRate=function(T){0>T||125<T?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):R=0<T?Math.floor(1e3/T):5},e.unstable_getCurrentPriorityLevel=function(){return d},e.unstable_getFirstCallbackNode=function(){return n(u)},e.unstable_next=function(T){switch(d){case 1:case 2:case 3:var z=3;break;default:z=d}var I=d;d=z;try{return T()}finally{d=I}},e.unstable_pauseExecution=function(){},e.unstable_requestPaint=function(){},e.unstable_runWithPriority=function(T,z){switch(T){case 1:case 2:case 3:case 4:case 5:break;default:T=3}var I=d;d=T;try{return z()}finally{d=I}},e.unstable_scheduleCallback=function(T,z,I){var K=e.unstable_now();switch(typeof I=="object"&&I!==null?(I=I.delay,I=typeof I=="number"&&0<I?K+I:K):I=K,T){case 1:var ee=-1;break;case 2:ee=250;break;case 5:ee=1073741823;break;case 4:ee=1e4;break;default:ee=5e3}return ee=I+ee,T={id:h++,callback:z,priorityLevel:T,startTime:I,expirationTime:ee,sortIndex:-1},I>K?(T.sortIndex=I,t(a,T),n(u)===null&&T===n(a)&&(w?(m(C),C=-1):w=!0,Fl(x,I-K))):(T.sortIndex=ee,t(u,T),g||y||(g=!0,Ml(E))),T},e.unstable_shouldYield=X,e.unstable_wrapCallback=function(T){var z=d;return function(){var I=d;d=z;try{return T.apply(this,arguments)}finally{d=I}}}})(ea);bu.exports=ea;var Wf=bu.exports;/**
|
*/(function(e){function t(T,z){var I=T.length;T.push(z);e:for(;0<I;){var K=I-1>>>1,ee=T[K];if(0<l(ee,z))T[K]=z,T[I]=ee,I=K;else break e}}function n(T){return T.length===0?null:T[0]}function r(T){if(T.length===0)return null;var z=T[0],I=T.pop();if(I!==z){T[0]=I;e:for(var K=0,ee=T.length,hr=ee>>>1;K<hr;){var Et=2*(K+1)-1,Dl=T[Et],Nt=Et+1,mr=T[Nt];if(0>l(Dl,I))Nt<ee&&0>l(mr,Dl)?(T[K]=mr,T[Nt]=I,K=Nt):(T[K]=Dl,T[Et]=I,K=Et);else if(Nt<ee&&0>l(mr,I))T[K]=mr,T[Nt]=I,K=Nt;else break e}}return z}function l(T,z){var I=T.sortIndex-z.sortIndex;return I!==0?I:T.id-z.id}if(typeof performance=="object"&&typeof performance.now=="function"){var i=performance;e.unstable_now=function(){return i.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var u=[],a=[],h=1,f=null,d=3,y=!1,g=!1,w=!1,C=typeof setTimeout=="function"?setTimeout:null,m=typeof clearTimeout=="function"?clearTimeout:null,p=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function v(T){for(var z=n(a);z!==null;){if(z.callback===null)r(a);else if(z.startTime<=T)r(a),z.sortIndex=z.expirationTime,t(u,z);else break;z=n(a)}}function x(T){if(w=!1,v(T),!g)if(n(u)!==null)g=!0,Ml(E);else{var z=n(a);z!==null&&Fl(x,z.startTime-T)}}function E(T,z){g=!1,w&&(w=!1,m(N),N=-1),y=!0;var I=d;try{for(v(z),f=n(u);f!==null&&(!(f.expirationTime>z)||T&&!X());){var K=f.callback;if(typeof K=="function"){f.callback=null,d=f.priorityLevel;var ee=K(f.expirationTime<=z);z=e.unstable_now(),typeof ee=="function"?f.callback=ee:f===n(u)&&r(u),v(z)}else r(u);f=n(u)}if(f!==null)var hr=!0;else{var Et=n(a);Et!==null&&Fl(x,Et.startTime-z),hr=!1}return hr}finally{f=null,d=I,y=!1}}var _=!1,P=null,N=-1,R=5,L=-1;function X(){return!(e.unstable_now()-L<R)}function Q(){if(P!==null){var T=e.unstable_now();L=T;var z=!0;try{z=P(!0,T)}finally{z?b():(_=!1,P=null)}}else _=!1}var b;if(typeof p=="function")b=function(){p(Q)};else if(typeof MessageChannel<"u"){var pr=new MessageChannel,xf=pr.port2;pr.port1.onmessage=Q,b=function(){xf.postMessage(null)}}else b=function(){C(Q,0)};function Ml(T){P=T,_||(_=!0,b())}function Fl(T,z){N=C(function(){T(e.unstable_now())},z)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(T){T.callback=null},e.unstable_continueExecution=function(){g||y||(g=!0,Ml(E))},e.unstable_forceFrameRate=function(T){0>T||125<T?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):R=0<T?Math.floor(1e3/T):5},e.unstable_getCurrentPriorityLevel=function(){return d},e.unstable_getFirstCallbackNode=function(){return n(u)},e.unstable_next=function(T){switch(d){case 1:case 2:case 3:var z=3;break;default:z=d}var I=d;d=z;try{return T()}finally{d=I}},e.unstable_pauseExecution=function(){},e.unstable_requestPaint=function(){},e.unstable_runWithPriority=function(T,z){switch(T){case 1:case 2:case 3:case 4:case 5:break;default:T=3}var I=d;d=T;try{return z()}finally{d=I}},e.unstable_scheduleCallback=function(T,z,I){var K=e.unstable_now();switch(typeof I=="object"&&I!==null?(I=I.delay,I=typeof I=="number"&&0<I?K+I:K):I=K,T){case 1:var ee=-1;break;case 2:ee=250;break;case 5:ee=1073741823;break;case 4:ee=1e4;break;default:ee=5e3}return ee=I+ee,T={id:h++,callback:z,priorityLevel:T,startTime:I,expirationTime:ee,sortIndex:-1},I>K?(T.sortIndex=I,t(a,T),n(u)===null&&T===n(a)&&(w?(m(N),N=-1):w=!0,Fl(x,I-K))):(T.sortIndex=ee,t(u,T),g||y||(g=!0,Ml(E))),T},e.unstable_shouldYield=X,e.unstable_wrapCallback=function(T){var z=d;return function(){var I=d;d=z;try{return T.apply(this,arguments)}finally{d=I}}}})(ta);ea.exports=ta;var Hf=ea.exports;/**
|
||||||
* @license React
|
* @license React
|
||||||
* react-dom.production.min.js
|
* react-dom.production.min.js
|
||||||
*
|
*
|
||||||
@@ -30,14 +30,14 @@ function xf(e,t){for(var n=0;n<t.length;n++){const r=t[n];if(typeof r!="string"&
|
|||||||
*
|
*
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/var Hf=S,ke=Wf;function k(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n<arguments.length;n++)t+="&args[]="+encodeURIComponent(arguments[n]);return"Minified React error #"+e+"; visit "+t+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}var ta=new Set,Bn={};function Dt(e,t){un(e,t),un(e+"Capture",t)}function un(e,t){for(Bn[e]=t,e=0;e<t.length;e++)ta.add(t[e])}var Je=!(typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),fi=Object.prototype.hasOwnProperty,Qf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,ps={},hs={};function Kf(e){return fi.call(hs,e)?!0:fi.call(ps,e)?!1:Qf.test(e)?hs[e]=!0:(ps[e]=!0,!1)}function Gf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Jf(e,t,n,r){if(t===null||typeof t>"u"||Gf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function pe(e,t,n,r,l,i,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=o}var ie={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ie[e]=new pe(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ie[t]=new pe(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ie[e]=new pe(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ie[e]=new pe(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ie[e]=new pe(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ie[e]=new pe(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ie[e]=new pe(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ie[e]=new pe(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ie[e]=new pe(e,5,!1,e.toLowerCase(),null,!1,!1)});var po=/[\-:]([a-z])/g;function ho(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(po,ho);ie[t]=new pe(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(po,ho);ie[t]=new pe(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(po,ho);ie[t]=new pe(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ie[e]=new pe(e,1,!1,e.toLowerCase(),null,!1,!1)});ie.xlinkHref=new pe("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ie[e]=new pe(e,1,!1,e.toLowerCase(),null,!0,!0)});function mo(e,t,n,r){var l=ie.hasOwnProperty(t)?ie[t]:null;(l!==null?l.type!==0:r||!(2<t.length)||t[0]!=="o"&&t[0]!=="
Line truncated
|
*/var Qf=S,ke=Hf;function k(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n<arguments.length;n++)t+="&args[]="+encodeURIComponent(arguments[n]);return"Minified React error #"+e+"; visit "+t+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}var na=new Set,Bn={};function Dt(e,t){un(e,t),un(e+"Capture",t)}function un(e,t){for(Bn[e]=t,e=0;e<t.length;e++)na.add(t[e])}var Je=!(typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),fi=Object.prototype.hasOwnProperty,Kf=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,ps={},hs={};function Gf(e){return fi.call(hs,e)?!0:fi.call(ps,e)?!1:Kf.test(e)?hs[e]=!0:(ps[e]=!0,!1)}function Jf(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function Yf(e,t,n,r){if(t===null||typeof t>"u"||Jf(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function pe(e,t,n,r,l,i,o){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=l,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=i,this.removeEmptyString=o}var ie={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){ie[e]=new pe(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];ie[t]=new pe(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){ie[e]=new pe(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){ie[e]=new pe(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){ie[e]=new pe(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){ie[e]=new pe(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){ie[e]=new pe(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){ie[e]=new pe(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){ie[e]=new pe(e,5,!1,e.toLowerCase(),null,!1,!1)});var po=/[\-:]([a-z])/g;function ho(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(po,ho);ie[t]=new pe(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(po,ho);ie[t]=new pe(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(po,ho);ie[t]=new pe(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){ie[e]=new pe(e,1,!1,e.toLowerCase(),null,!1,!1)});ie.xlinkHref=new pe("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){ie[e]=new pe(e,1,!1,e.toLowerCase(),null,!0,!0)});function mo(e,t,n,r){var l=ie.hasOwnProperty(t)?ie[t]:null;(l!==null?l.type!==0:r||!(2<t.length)||t[0]!=="o"&&t[0]!=="
Line truncated
|
||||||
`+Ul+e}var Al=!1;function Vl(e,t){if(!e||Al)return"";Al=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(t)if(t=function(){throw Error()},Object.defineProperty(t.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(t,[])}catch(a){var r=a}Reflect.construct(e,[],t)}else{try{t.call()}catch(a){r=a}e.call(t.prototype)}else{try{throw Error()}catch(a){r=a}e()}}catch(a){if(a&&r&&typeof a.stack=="string"){for(var l=a.stack.split(`
|
`+Ul+e}var Al=!1;function Vl(e,t){if(!e||Al)return"";Al=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(t)if(t=function(){throw Error()},Object.defineProperty(t.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(t,[])}catch(a){var r=a}Reflect.construct(e,[],t)}else{try{t.call()}catch(a){r=a}e.call(t.prototype)}else{try{throw Error()}catch(a){r=a}e()}}catch(a){if(a&&r&&typeof a.stack=="string"){for(var l=a.stack.split(`
|
||||||
`),i=r.stack.split(`
|
`),i=r.stack.split(`
|
||||||
`),o=l.length-1,s=i.length-1;1<=o&&0<=s&&l[o]!==i[s];)s--;for(;1<=o&&0<=s;o--,s--)if(l[o]!==i[s]){if(o!==1||s!==1)do if(o--,s--,0>s||l[o]!==i[s]){var u=`
|
`),o=l.length-1,s=i.length-1;1<=o&&0<=s&&l[o]!==i[s];)s--;for(;1<=o&&0<=s;o--,s--)if(l[o]!==i[s]){if(o!==1||s!==1)do if(o--,s--,0>s||l[o]!==i[s]){var u=`
|
||||||
`+l[o].replace(" at new "," at ");return e.displayName&&u.includes("<anonymous>")&&(u=u.replace("<anonymous>",e.displayName)),u}while(1<=o&&0<=s);break}}}finally{Al=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Pn(e):""}function Yf(e){switch(e.tag){case 5:return Pn(e.type);case 16:return Pn("Lazy");case 13:return Pn("Suspense");case 19:return Pn("SuspenseList");case 0:case 2:case 15:return e=Vl(e.type,!1),e;case 11:return e=Vl(e.type.render,!1),e;case 1:return e=Vl(e.type,!0),e;default:return""}}function mi(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Wt:return"Fragment";case Bt:return"Portal";case di:return"Profiler";case vo:return"StrictMode";case pi:return"Suspense";case hi:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case ra:return(e.displayName||"Context")+".Consumer";case na:return(e._context.displayName||"Context")+".Provider";case go:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case yo:return t=e.displayName||null,t!==null?t:mi(e.type)||"Memo";case tt:t=e._payload,e=e._init;try{return mi(e(t))}catch{}}return null}function Xf(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return mi(t);case 8:return t===vo?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function gt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function ia(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Zf(e){var t=ia(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(o){r=""+o,i.call(this,o)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(o){r=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function yr(e){e._valueTracker||(e._valueTracker=Zf(e))}function oa(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=ia(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Gr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function vi(e,t){var n=t.checked;return W({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function vs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=gt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function sa(e,t){t=t.checked,t!=null&&mo(e,"checked",t,!1)}function gi(e,t){sa(e,t);var n=gt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?yi(e,t.type,n):t.hasOwnProperty("defaultValue")&&yi(e,t.type,gt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function gs(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function yi(e,t,n){(t!=="number"||Gr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Tn=Array.isArray;function tn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l<n.length;l++)t["$"+n[l]]=!0;for(n=0;n<e.length;n++)l=t.hasOwnProperty("$"+e[n].value),e[n].selected!==l&&(e[n].selected=l),l&&r&&(e[n].defaultSelected=!0)}else{for(n=""+gt(n),t=null,l=0;l<e.length;l++){if(e[l].value===n){e[l].selected=!0,r&&(e[l].defaultSelected=!0);return}t!==null||e[l].disabled||(t=e[l])}t!==null&&(t.selected=!0)}}function xi(e,t){if(t.dang
Line truncated
|
`+l[o].replace(" at new "," at ");return e.displayName&&u.includes("<anonymous>")&&(u=u.replace("<anonymous>",e.displayName)),u}while(1<=o&&0<=s);break}}}finally{Al=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?Pn(e):""}function Xf(e){switch(e.tag){case 5:return Pn(e.type);case 16:return Pn("Lazy");case 13:return Pn("Suspense");case 19:return Pn("SuspenseList");case 0:case 2:case 15:return e=Vl(e.type,!1),e;case 11:return e=Vl(e.type.render,!1),e;case 1:return e=Vl(e.type,!0),e;default:return""}}function mi(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case Wt:return"Fragment";case Bt:return"Portal";case di:return"Profiler";case vo:return"StrictMode";case pi:return"Suspense";case hi:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case la:return(e.displayName||"Context")+".Consumer";case ra:return(e._context.displayName||"Context")+".Provider";case go:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case yo:return t=e.displayName||null,t!==null?t:mi(e.type)||"Memo";case tt:t=e._payload,e=e._init;try{return mi(e(t))}catch{}}return null}function Zf(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return mi(t);case 8:return t===vo?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function gt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function oa(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function qf(e){var t=oa(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var l=n.get,i=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return l.call(this)},set:function(o){r=""+o,i.call(this,o)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(o){r=""+o},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function yr(e){e._valueTracker||(e._valueTracker=qf(e))}function sa(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=oa(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Gr(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function vi(e,t){var n=t.checked;return W({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function vs(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=gt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function ua(e,t){t=t.checked,t!=null&&mo(e,"checked",t,!1)}function gi(e,t){ua(e,t);var n=gt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?yi(e,t.type,n):t.hasOwnProperty("defaultValue")&&yi(e,t.type,gt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function gs(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function yi(e,t,n){(t!=="number"||Gr(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var Tn=Array.isArray;function tn(e,t,n,r){if(e=e.options,t){t={};for(var l=0;l<n.length;l++)t["$"+n[l]]=!0;for(n=0;n<e.length;n++)l=t.hasOwnProperty("$"+e[n].value),e[n].selected!==l&&(e[n].selected=l),l&&r&&(e[n].defaultSelected=!0)}else{for(n=""+gt(n),t=null,l=0;l<e.length;l++){if(e[l].value===n){e[l].selected=!0,r&&(e[l].defaultSelected=!0);return}t!==null||e[l].disabled||(t=e[l])}t!==null&&(t.selected=!0)}}function xi(e,t){if(t.dang
Line truncated
|
||||||
`).replace(sp,"")}function jr(e,t,n){if(t=As(t),As(e)!==t&&n)throw Error(k(425))}function tl(){}var zi=null,Ii=null;function Oi(e,t){return e==="textarea"||e==="noscript"||typeof t.children=="string"||typeof t.children=="number"||typeof t.dangerouslySetInnerHTML=="object"&&t.dangerouslySetInnerHTML!==null&&t.dangerouslySetInnerHTML.__html!=null}var Mi=typeof setTimeout=="function"?setTimeout:void 0,up=typeof clearTimeout=="function"?clearTimeout:void 0,Vs=typeof Promise=="function"?Promise:void 0,ap=typeof queueMicrotask=="function"?queueMicrotask:typeof Vs<"u"?function(e){return Vs.resolve(null).then(e).catch(cp)}:Mi;function cp(e){setTimeout(function(){throw e})}function ei(e,t){var n=t,r=0;do{var l=n.nextSibling;if(e.removeChild(n),l&&l.nodeType===8)if(n=l.data,n==="/$"){if(r===0){e.removeChild(l),Gn(t);return}r--}else n!=="$"&&n!=="$?"&&n!=="$!"||r++;n=l}while(n);Gn(t)}function ft(e){for(;e!=null;e=e.nextSibling){var t=e.nodeType;if(t===1||t===3)break;if(t===8){if(t=e.data,t==="$"||t==="$!"||t==="$?")break;if(t==="/$")return null}}return e}function Bs(e){e=e.previousSibling;for(var t=0;e;){if(e.nodeType===8){var n=e.data;if(n==="$"||n==="$!"||n==="$?"){if(t===0)return e;t--}else n==="/$"&&t++}e=e.previousSibling}return null}var gn=Math.random().toString(36).slice(2),Ae="__reactFiber$"+gn,qn="__reactProps$"+gn,Ye="__reactContainer$"+gn,Fi="__reactEvents$"+gn,fp="__reactListeners$"+gn,dp="__reactHandles$"+gn;function _t(e){var t=e[Ae];if(t)return t;for(var n=e.parentNode;n;){if(t=n[Ye]||n[Ae]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=Bs(e);e!==null;){if(n=e[Ae])return n;e=Bs(e)}return t}e=n,n=e.parentNode}return null}function cr(e){return e=e[Ae]||e[Ye],!e||e.tag!==5&&e.tag!==6&&e.tag!==13&&e.tag!==3?null:e}function Gt(e){if(e.tag===5||e.tag===6)return e.stateNode;throw Error(k(33))}function El(e){return e[qn]||null}var Di=[],Jt=-1;function wt(e){return{current:e}}function U(e){0>Jt||(e.current=Di[Jt],Di[Jt]=null,Jt--)}function D(e,t){Jt++,Di[Jt]=e.current,e.current=t}var yt={},ae=wt(yt),ve=wt(!1),zt=yt;function an(e,t){var n=e.type.contextTypes;if(!n)return yt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function ge(e){return e=e.childContextTypes,e!=null}function nl(){U(ve),U(ae)}function Ws(e,t,n){if(ae.current!==yt)throw Error(k(168));D(ae,t),D(ve,n)}function Ya(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(k(108,Xf(e)||"Unknown",l));return W({},n,r)}function rl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||yt,zt=ae.current,D(ae,e),D(ve,ve.current),!0}function Hs(e,t,n){var r=e.stateNode;if(!r)throw Error(k(169));n?(e=Ya(e,t,zt),r.__reactInternalMemoizedMergedChildContext=e,U(ve),U(ae),D(ae,e)):U(ve),D(ve,n)}var He=null,Cl=!1,ti=!1;function Xa(e){He===null?He=[e]:He.push(e)}function pp(e){Cl=!0,Xa(e)}function St(){if(!ti&&He!==null){ti=!0;var e=0,t=F;try{var n=He;for(F=1;e<n.length;e++){var r=n[e];do r=r(!0);while(r!==null)}He=null,Cl=!1}catch(l){throw He!==null&&(He=He.slice(e+1)),Sa(wo,St),l}finally{F=t,ti=!1}}return null}var Yt=[],Xt=0,ll=null,il=0,Ne=[],je=0,It=null,Qe=1,Ke="";function Nt(e,t){Yt[Xt++]=il,Yt[Xt++]=ll,ll=e,il=t}function Za(e,t,n){Ne[je++]=Qe,Ne[je++]=Ke,Ne[je++]=It,It=e;var r=Qe;e=Ke;var l=32-Me(r)-1;r&=~(1<<l),n+=1;var i=32-Me(t)+l;if(30<i){var o=l-l%5;i=(r&(1<<o)-1).toString(32),r>>=o,l-=o,Qe=1<<32-Me(t)+l|n<<l|r,Ke=i+e}else Qe=1<<i|n<<l|r,Ke=e}function To(e){e.return!==null&&(Nt(e,1),Za(e,1,0))}function Lo(e){for(;e===ll;)ll=Yt[--Xt],Yt[Xt]=null,il=Yt[--Xt],Yt[Xt]=null;for(;e===It;)It=Ne[--je],Ne[je]=null,Ke=Ne[--je],Ne[je]=null,Qe=Ne[--je],Ne[je]=null}var Se=null,we=null,A=!1,Oe=null;function qa(e,t){var n=_e(5,null,null,0);n.elementType="DELETED",n.stateNode=t,n.return=e,t=e.deletions,t===null?(e.deletions=[n],e.flags|=16):t.push(n)}function Qs(e,t){switch(e.tag){case 5:var n=e.type;return t=t.nodeType!==1||n.toLowerCase()!==t.nodeName.toLowerCase()?null:t,t!==null?(e.stateNode=t,Se=e,we=ft(t.firstChild),!0):!1;case 6:return t=e.pendingProps===""||t.nodeType!==3?null:t,t!==null?(e.stateNode=t,Se=e,we=null,!0):!1;case 13:return t=t.nodeType!==8?null:t,t!==null?(n=It!==null?{id:Qe,overflow:Ke}:null,e.memoizedState={dehydrated:t,treeContext:n,retryLane:1073741824},n=_e(18,null,null,0),n.stateNode=t,n.return=e,e.child=n,Se=e,we=null,!0):!1;default:return!1}}function $i(e){return(e.mode&1)!==0&&(e.flags&128)===0}function Ui(e){if(A){var t=we;if(t){var n=t;if(!Qs(e,t)){if($i(e))throw Error(k(418));t=ft(n.nextSibling);var r=Se;t&&Qs(e,t)?qa(r,n):(e.flags=e.flags&-4097|2,A=!1,Se=e)}}else{if($i(e))throw Error(k(418));e.flags=e.flags&-4097|2,A=!1,Se=e}}}function Ks(e){for(e=e.return;e!==null&&e.t
Line truncated
|
`).replace(up,"")}function jr(e,t,n){if(t=As(t),As(e)!==t&&n)throw Error(k(425))}function tl(){}var zi=null,Ii=null;function Oi(e,t){return e==="textarea"||e==="noscript"||typeof t.children=="string"||typeof t.children=="number"||typeof t.dangerouslySetInnerHTML=="object"&&t.dangerouslySetInnerHTML!==null&&t.dangerouslySetInnerHTML.__html!=null}var Mi=typeof setTimeout=="function"?setTimeout:void 0,ap=typeof clearTimeout=="function"?clearTimeout:void 0,Vs=typeof Promise=="function"?Promise:void 0,cp=typeof queueMicrotask=="function"?queueMicrotask:typeof Vs<"u"?function(e){return Vs.resolve(null).then(e).catch(fp)}:Mi;function fp(e){setTimeout(function(){throw e})}function ei(e,t){var n=t,r=0;do{var l=n.nextSibling;if(e.removeChild(n),l&&l.nodeType===8)if(n=l.data,n==="/$"){if(r===0){e.removeChild(l),Gn(t);return}r--}else n!=="$"&&n!=="$?"&&n!=="$!"||r++;n=l}while(n);Gn(t)}function ft(e){for(;e!=null;e=e.nextSibling){var t=e.nodeType;if(t===1||t===3)break;if(t===8){if(t=e.data,t==="$"||t==="$!"||t==="$?")break;if(t==="/$")return null}}return e}function Bs(e){e=e.previousSibling;for(var t=0;e;){if(e.nodeType===8){var n=e.data;if(n==="$"||n==="$!"||n==="$?"){if(t===0)return e;t--}else n==="/$"&&t++}e=e.previousSibling}return null}var gn=Math.random().toString(36).slice(2),Ae="__reactFiber$"+gn,qn="__reactProps$"+gn,Ye="__reactContainer$"+gn,Fi="__reactEvents$"+gn,dp="__reactListeners$"+gn,pp="__reactHandles$"+gn;function _t(e){var t=e[Ae];if(t)return t;for(var n=e.parentNode;n;){if(t=n[Ye]||n[Ae]){if(n=t.alternate,t.child!==null||n!==null&&n.child!==null)for(e=Bs(e);e!==null;){if(n=e[Ae])return n;e=Bs(e)}return t}e=n,n=e.parentNode}return null}function cr(e){return e=e[Ae]||e[Ye],!e||e.tag!==5&&e.tag!==6&&e.tag!==13&&e.tag!==3?null:e}function Gt(e){if(e.tag===5||e.tag===6)return e.stateNode;throw Error(k(33))}function El(e){return e[qn]||null}var Di=[],Jt=-1;function wt(e){return{current:e}}function U(e){0>Jt||(e.current=Di[Jt],Di[Jt]=null,Jt--)}function D(e,t){Jt++,Di[Jt]=e.current,e.current=t}var yt={},ae=wt(yt),ve=wt(!1),zt=yt;function an(e,t){var n=e.type.contextTypes;if(!n)return yt;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var l={},i;for(i in n)l[i]=t[i];return r&&(e=e.stateNode,e.__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=l),l}function ge(e){return e=e.childContextTypes,e!=null}function nl(){U(ve),U(ae)}function Ws(e,t,n){if(ae.current!==yt)throw Error(k(168));D(ae,t),D(ve,n)}function Xa(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,typeof r.getChildContext!="function")return n;r=r.getChildContext();for(var l in r)if(!(l in t))throw Error(k(108,Zf(e)||"Unknown",l));return W({},n,r)}function rl(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||yt,zt=ae.current,D(ae,e),D(ve,ve.current),!0}function Hs(e,t,n){var r=e.stateNode;if(!r)throw Error(k(169));n?(e=Xa(e,t,zt),r.__reactInternalMemoizedMergedChildContext=e,U(ve),U(ae),D(ae,e)):U(ve),D(ve,n)}var He=null,Nl=!1,ti=!1;function Za(e){He===null?He=[e]:He.push(e)}function hp(e){Nl=!0,Za(e)}function St(){if(!ti&&He!==null){ti=!0;var e=0,t=F;try{var n=He;for(F=1;e<n.length;e++){var r=n[e];do r=r(!0);while(r!==null)}He=null,Nl=!1}catch(l){throw He!==null&&(He=He.slice(e+1)),ka(wo,St),l}finally{F=t,ti=!1}}return null}var Yt=[],Xt=0,ll=null,il=0,Ce=[],je=0,It=null,Qe=1,Ke="";function Ct(e,t){Yt[Xt++]=il,Yt[Xt++]=ll,ll=e,il=t}function qa(e,t,n){Ce[je++]=Qe,Ce[je++]=Ke,Ce[je++]=It,It=e;var r=Qe;e=Ke;var l=32-Me(r)-1;r&=~(1<<l),n+=1;var i=32-Me(t)+l;if(30<i){var o=l-l%5;i=(r&(1<<o)-1).toString(32),r>>=o,l-=o,Qe=1<<32-Me(t)+l|n<<l|r,Ke=i+e}else Qe=1<<i|n<<l|r,Ke=e}function To(e){e.return!==null&&(Ct(e,1),qa(e,1,0))}function Lo(e){for(;e===ll;)ll=Yt[--Xt],Yt[Xt]=null,il=Yt[--Xt],Yt[Xt]=null;for(;e===It;)It=Ce[--je],Ce[je]=null,Ke=Ce[--je],Ce[je]=null,Qe=Ce[--je],Ce[je]=null}var Se=null,we=null,A=!1,Oe=null;function ba(e,t){var n=_e(5,null,null,0);n.elementType="DELETED",n.stateNode=t,n.return=e,t=e.deletions,t===null?(e.deletions=[n],e.flags|=16):t.push(n)}function Qs(e,t){switch(e.tag){case 5:var n=e.type;return t=t.nodeType!==1||n.toLowerCase()!==t.nodeName.toLowerCase()?null:t,t!==null?(e.stateNode=t,Se=e,we=ft(t.firstChild),!0):!1;case 6:return t=e.pendingProps===""||t.nodeType!==3?null:t,t!==null?(e.stateNode=t,Se=e,we=null,!0):!1;case 13:return t=t.nodeType!==8?null:t,t!==null?(n=It!==null?{id:Qe,overflow:Ke}:null,e.memoizedState={dehydrated:t,treeContext:n,retryLane:1073741824},n=_e(18,null,null,0),n.stateNode=t,n.return=e,e.child=n,Se=e,we=null,!0):!1;default:return!1}}function $i(e){return(e.mode&1)!==0&&(e.flags&128)===0}function Ui(e){if(A){var t=we;if(t){var n=t;if(!Qs(e,t)){if($i(e))throw Error(k(418));t=ft(n.nextSibling);var r=Se;t&&Qs(e,t)?ba(r,n):(e.flags=e.flags&-4097|2,A=!1,Se=e)}}else{if($i(e))throw Error(k(418));e.flags=e.flags&-4097|2,A=!1,Se=e}}}function Ks(e){for(e=e.return;e!==null&&e.t
Line truncated
|
||||||
Error generating stack: `+i.message+`
|
Error generating stack: `+i.message+`
|
||||||
`+i.stack}return{value:e,source:t,stack:l,digest:null}}function oi(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function Wi(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var kp=typeof WeakMap=="function"?WeakMap:Map;function Nc(e,t,n){n=Ge(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){pl||(pl=!0,bi=r),Wi(e,t)},n}function jc(e,t,n){n=Ge(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var l=t.value;n.payload=function(){return r(l)},n.callback=function(){Wi(e,t)}}var i=e.stateNode;return i!==null&&typeof i.componentDidCatch=="function"&&(n.callback=function(){Wi(e,t),typeof r!="function"&&(pt===null?pt=new Set([this]):pt.add(this));var o=t.stack;this.componentDidCatch(t.value,{componentStack:o!==null?o:""})}),n}function eu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new kp;var l=new Set;r.set(t,l)}else l=r.get(t),l===void 0&&(l=new Set,r.set(t,l));l.has(n)||(l.add(n),e=Fp.bind(null,e,t,n),t.then(e,e))}function tu(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function nu(e,t,n,r,l){return e.mode&1?(e.flags|=65536,e.lanes=l,e):(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=Ge(-1,1),t.tag=2,dt(n,t,1))),n.lanes|=1),e)}var Ep=qe.ReactCurrentOwner,me=!1;function ce(e,t,n,r){t.child=e===null?tc(t,null,n,r):fn(t,e.child,n,r)}function ru(e,t,n,r,l){n=n.render;var i=t.ref;return on(t,l),r=Vo(e,t,n,r,i,l),n=Bo(),e!==null&&!me?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Ze(e,t,l)):(A&&n&&To(t),t.flags|=1,ce(e,t,r,l),t.child)}function lu(e,t,n,r,l){if(e===null){var i=n.type;return typeof i=="function"&&!qo(i)&&i.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=i,_c(e,t,i,r,l)):(e=Kr(n.type,null,r,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(i=e.child,!(e.lanes&l)){var o=i.memoizedProps;if(n=n.compare,n=n!==null?n:Yn,n(o,r)&&e.ref===t.ref)return Ze(e,t,l)}return t.flags|=1,e=mt(i,r),e.ref=t.ref,e.return=t,t.child=e}function _c(e,t,n,r,l){if(e!==null){var i=e.memoizedProps;if(Yn(i,r)&&e.ref===t.ref)if(me=!1,t.pendingProps=r=i,(e.lanes&l)!==0)e.flags&131072&&(me=!0);else return t.lanes=e.lanes,Ze(e,t,l)}return Hi(e,t,n,r,l)}function Pc(e,t,n){var r=t.pendingProps,l=r.children,i=e!==null?e.memoizedState:null;if(r.mode==="hidden")if(!(t.mode&1))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},D(bt,xe),xe|=n;else{if(!(n&1073741824))return e=i!==null?i.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,D(bt,xe),xe|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=i!==null?i.baseLanes:n,D(bt,xe),xe|=r}else i!==null?(r=i.baseLanes|n,t.memoizedState=null):r=n,D(bt,xe),xe|=r;return ce(e,t,l,n),t.child}function Tc(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function Hi(e,t,n,r,l){var i=ge(n)?zt:ae.current;return i=an(t,i),on(t,l),n=Vo(e,t,n,r,i,l),r=Bo(),e!==null&&!me?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Ze(e,t,l)):(A&&r&&To(t),t.flags|=1,ce(e,t,n,l),t.child)}function iu(e,t,n,r,l){if(ge(n)){var i=!0;rl(t)}else i=!1;if(on(t,l),t.stateNode===null)Wr(e,t),Cc(t,n,r),Bi(t,n,r,l),r=!0;else if(e===null){var o=t.stateNode,s=t.memoizedProps;o.props=s;var u=o.context,a=n.contextType;typeof a=="object"&&a!==null?a=Te(a):(a=ge(n)?zt:ae.current,a=an(t,a));var h=n.getDerivedStateFromProps,c=typeof h=="function"||typeof o.getSnapshotBeforeUpdate=="function";c||typeof o.UNSAFE_componentWillReceiveProps!="function"&&typeof o.componentWillReceiveProps!="function"||(s!==r||u!==a)&&bs(t,o,r,a),nt=!1;var d=t.memoizedState;o.state=d,ul(t,r,o,l),u=t.memoizedState,s!==r||d!==u||ve.current||nt?(typeof h=="function"&&(Vi(t,n,h,r),u=t.memoizedState),(s=nt||qs(t,n,s,r,d,u,a))?(c||typeof o.UNSAFE_componentWillMount!="function"&&typeof o.componentWillMount!="function"||(typeof o.componentWillMount=="function"&&o.componentWillMount(),typeof o.UNSAFE_componentWillMount=="function"&&o.UNSAFE_componentWillMount()),typeof o.componentDidMount=="function"&&(t.flags|=4194308)):(typeof o.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=u),o.props=r,o.state=u,o.context=a,r=s):(typeof o.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{o=t.stateNode,rc(e,t),s=t.memoizedProps,a=t.type===t.elementType?s:ze(t.type,s),o.props=a,c=t.pendingProps,d=o.context,u=n.contextType,typeof u=="object"&&u!==null?u=Te(u):(u=ge(n)?zt:ae.current,u=an(t,u));var y=n.getDerivedStateFromProps;(h=typeof y=="function"||typeof o.getSnapshotBeforeUpdate=="function")||typeof o.UNSAFE_componentWillReceiveProps!="function"&&typeof o.componentWillReceiveProps!="function"||(s!==c||d!==u)&&bs(t,o,r,u),nt=!1,d=t.memoizedState,o.state=d,ul(t,r,o,l);var g=t.memoizedState;s
Line truncated
|
`+i.stack}return{value:e,source:t,stack:l,digest:null}}function oi(e,t,n){return{value:e,source:null,stack:n??null,digest:t??null}}function Wi(e,t){try{console.error(t.value)}catch(n){setTimeout(function(){throw n})}}var Ep=typeof WeakMap=="function"?WeakMap:Map;function jc(e,t,n){n=Ge(-1,n),n.tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){pl||(pl=!0,bi=r),Wi(e,t)},n}function _c(e,t,n){n=Ge(-1,n),n.tag=3;var r=e.type.getDerivedStateFromError;if(typeof r=="function"){var l=t.value;n.payload=function(){return r(l)},n.callback=function(){Wi(e,t)}}var i=e.stateNode;return i!==null&&typeof i.componentDidCatch=="function"&&(n.callback=function(){Wi(e,t),typeof r!="function"&&(pt===null?pt=new Set([this]):pt.add(this));var o=t.stack;this.componentDidCatch(t.value,{componentStack:o!==null?o:""})}),n}function eu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Ep;var l=new Set;r.set(t,l)}else l=r.get(t),l===void 0&&(l=new Set,r.set(t,l));l.has(n)||(l.add(n),e=Dp.bind(null,e,t,n),t.then(e,e))}function tu(e){do{var t;if((t=e.tag===13)&&(t=e.memoizedState,t=t!==null?t.dehydrated!==null:!0),t)return e;e=e.return}while(e!==null);return null}function nu(e,t,n,r,l){return e.mode&1?(e.flags|=65536,e.lanes=l,e):(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,n.tag===1&&(n.alternate===null?n.tag=17:(t=Ge(-1,1),t.tag=2,dt(n,t,1))),n.lanes|=1),e)}var Np=qe.ReactCurrentOwner,me=!1;function ce(e,t,n,r){t.child=e===null?nc(t,null,n,r):fn(t,e.child,n,r)}function ru(e,t,n,r,l){n=n.render;var i=t.ref;return on(t,l),r=Vo(e,t,n,r,i,l),n=Bo(),e!==null&&!me?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Ze(e,t,l)):(A&&n&&To(t),t.flags|=1,ce(e,t,r,l),t.child)}function lu(e,t,n,r,l){if(e===null){var i=n.type;return typeof i=="function"&&!qo(i)&&i.defaultProps===void 0&&n.compare===null&&n.defaultProps===void 0?(t.tag=15,t.type=i,Pc(e,t,i,r,l)):(e=Kr(n.type,null,r,t,t.mode,l),e.ref=t.ref,e.return=t,t.child=e)}if(i=e.child,!(e.lanes&l)){var o=i.memoizedProps;if(n=n.compare,n=n!==null?n:Yn,n(o,r)&&e.ref===t.ref)return Ze(e,t,l)}return t.flags|=1,e=mt(i,r),e.ref=t.ref,e.return=t,t.child=e}function Pc(e,t,n,r,l){if(e!==null){var i=e.memoizedProps;if(Yn(i,r)&&e.ref===t.ref)if(me=!1,t.pendingProps=r=i,(e.lanes&l)!==0)e.flags&131072&&(me=!0);else return t.lanes=e.lanes,Ze(e,t,l)}return Hi(e,t,n,r,l)}function Tc(e,t,n){var r=t.pendingProps,l=r.children,i=e!==null?e.memoizedState:null;if(r.mode==="hidden")if(!(t.mode&1))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},D(bt,xe),xe|=n;else{if(!(n&1073741824))return e=i!==null?i.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,D(bt,xe),xe|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=i!==null?i.baseLanes:n,D(bt,xe),xe|=r}else i!==null?(r=i.baseLanes|n,t.memoizedState=null):r=n,D(bt,xe),xe|=r;return ce(e,t,l,n),t.child}function Lc(e,t){var n=t.ref;(e===null&&n!==null||e!==null&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function Hi(e,t,n,r,l){var i=ge(n)?zt:ae.current;return i=an(t,i),on(t,l),n=Vo(e,t,n,r,i,l),r=Bo(),e!==null&&!me?(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~l,Ze(e,t,l)):(A&&r&&To(t),t.flags|=1,ce(e,t,n,l),t.child)}function iu(e,t,n,r,l){if(ge(n)){var i=!0;rl(t)}else i=!1;if(on(t,l),t.stateNode===null)Wr(e,t),Cc(t,n,r),Bi(t,n,r,l),r=!0;else if(e===null){var o=t.stateNode,s=t.memoizedProps;o.props=s;var u=o.context,a=n.contextType;typeof a=="object"&&a!==null?a=Te(a):(a=ge(n)?zt:ae.current,a=an(t,a));var h=n.getDerivedStateFromProps,f=typeof h=="function"||typeof o.getSnapshotBeforeUpdate=="function";f||typeof o.UNSAFE_componentWillReceiveProps!="function"&&typeof o.componentWillReceiveProps!="function"||(s!==r||u!==a)&&bs(t,o,r,a),nt=!1;var d=t.memoizedState;o.state=d,ul(t,r,o,l),u=t.memoizedState,s!==r||d!==u||ve.current||nt?(typeof h=="function"&&(Vi(t,n,h,r),u=t.memoizedState),(s=nt||qs(t,n,s,r,d,u,a))?(f||typeof o.UNSAFE_componentWillMount!="function"&&typeof o.componentWillMount!="function"||(typeof o.componentWillMount=="function"&&o.componentWillMount(),typeof o.UNSAFE_componentWillMount=="function"&&o.UNSAFE_componentWillMount()),typeof o.componentDidMount=="function"&&(t.flags|=4194308)):(typeof o.componentDidMount=="function"&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=u),o.props=r,o.state=u,o.context=a,r=s):(typeof o.componentDidMount=="function"&&(t.flags|=4194308),r=!1)}else{o=t.stateNode,lc(e,t),s=t.memoizedProps,a=t.type===t.elementType?s:ze(t.type,s),o.props=a,f=t.pendingProps,d=o.context,u=n.contextType,typeof u=="object"&&u!==null?u=Te(u):(u=ge(n)?zt:ae.current,u=an(t,u));var y=n.getDerivedStateFromProps;(h=typeof y=="function"||typeof o.getSnapshotBeforeUpdate=="function")||typeof o.UNSAFE_componentWillReceiveProps!="function"&&typeof o.componentWillReceiveProps!="function"||(s!==f||d!==u)&&bs(t,o,r,u),nt=!1,d=t.memoizedState,o.state=d,ul(t,r,o,l);var g=t.memoizedState;s
Line truncated
|
||||||
* @remix-run/router v1.23.2
|
* @remix-run/router v1.23.2
|
||||||
*
|
*
|
||||||
* Copyright (c) Remix Software Inc.
|
* Copyright (c) Remix Software Inc.
|
||||||
@@ -46,7 +46,7 @@ Error generating stack: `+i.message+`
|
|||||||
* LICENSE.md file in the root directory of this source tree.
|
* LICENSE.md file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/function ir(){return ir=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},ir.apply(this,arguments)}var st;(function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"})(st||(st={}));const xu="popstate";function Jp(e){e===void 0&&(e={});function t(r,l){let{pathname:i,search:o,hash:s}=r.location;return ro("",{pathname:i,search:o,hash:s},l.state&&l.state.usr||null,l.state&&l.state.key||"default")}function n(r,l){return typeof l=="string"?l:gl(l)}return Xp(t,n,null,e)}function J(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function rs(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function Yp(){return Math.random().toString(36).substr(2,8)}function wu(e,t){return{usr:e.state,key:e.key,idx:t}}function ro(e,t,n,r){return n===void 0&&(n=null),ir({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof t=="string"?yn(t):t,{state:n,key:t&&t.key||r||Yp()})}function gl(e){let{pathname:t="/",search:n="",hash:r=""}=e;return n&&n!=="?"&&(t+=n.charAt(0)==="?"?n:"?"+n),r&&r!=="#"&&(t+=r.charAt(0)==="#"?r:"#"+r),t}function yn(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substr(n),e=e.substr(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substr(r),e=e.substr(0,r)),e&&(t.pathname=e)}return t}function Xp(e,t,n,r){r===void 0&&(r={});let{window:l=document.defaultView,v5Compat:i=!1}=r,o=l.history,s=st.Pop,u=null,a=h();a==null&&(a=0,o.replaceState(ir({},o.state,{idx:a}),""));function h(){return(o.state||{idx:null}).idx}function c(){s=st.Pop;let N=h(),m=N==null?null:N-a;a=N,u&&u({action:s,location:w.location,delta:m})}function d(N,m){s=st.Push;let f=ro(w.location,N,m);a=h()+1;let v=wu(f,a),x=w.createHref(f);try{o.pushState(v,"",x)}catch(E){if(E instanceof DOMException&&E.name==="DataCloneError")throw E;l.location.assign(x)}i&&u&&u({action:s,location:w.location,delta:1})}function y(N,m){s=st.Replace;let f=ro(w.location,N,m);a=h();let v=wu(f,a),x=w.createHref(f);o.replaceState(v,"",x),i&&u&&u({action:s,location:w.location,delta:0})}function g(N){let m=l.location.origin!=="null"?l.location.origin:l.location.href,f=typeof N=="string"?N:gl(N);return f=f.replace(/ $/,"%20"),J(m,"No window.location.(origin|href) available to create URL for href: "+f),new URL(f,m)}let w={get action(){return s},get location(){return e(l,o)},listen(N){if(u)throw new Error("A history only accepts one active listener");return l.addEventListener(xu,c),u=N,()=>{l.removeEventListener(xu,c),u=null}},createHref(N){return t(l,N)},createURL:g,encodeLocation(N){let m=g(N);return{pathname:m.pathname,search:m.search,hash:m.hash}},push:d,replace:y,go(N){return o.go(N)}};return w}var Su;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(Su||(Su={}));function Zp(e,t,n){return n===void 0&&(n="/"),qp(e,t,n)}function qp(e,t,n,r){let l=typeof t=="string"?yn(t):t,i=ls(l.pathname||"/",n);if(i==null)return null;let o=ef(e);bp(o);let s=null;for(let u=0;s==null&&u<o.length;++u){let a=fh(i);s=uh(o[u],a)}return s}function ef(e,t,n,r){t===void 0&&(t=[]),n===void 0&&(n=[]),r===void 0&&(r="");let l=(i,o,s)=>{let u={relativePath:s===void 0?i.path||"":s,caseSensitive:i.caseSensitive===!0,childrenIndex:o,route:i};u.relativePath.startsWith("/")&&(J(u.relativePath.startsWith(r),'Absolute route path "'+u.relativePath+'" nested under path '+('"'+r+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),u.relativePath=u.relativePath.slice(r.length));let a=vt([r,u.relativePath]),h=n.concat(u);i.children&&i.children.length>0&&(J(i.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+a+'".')),ef(i.children,t,h,a)),!(i.path==null&&!i.index)&&t.push({path:a,score:oh(a,i.index),routesMeta:h})};return e.forEach((i,o)=>{var s;if(i.path===""||!((s=i.path)!=null&&s.includes("?")))l(i,o);else for(let u of tf(i.path))l(i,o,u)}),t}function tf(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,l=n.endsWith("?"),i=n.replace(/\?$/,"");if(r.length===0)return l?[i,""]:[i];let o=tf(r.join("/")),s=[];return s.push(...o.map(u=>u===""?i:[i,u].join("/"))),l&&s.push(...o),s.map(u=>e.startsWith("/")&&u===""?"/":u)}function bp(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:sh(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}const eh=/^:[\w-]+$/,th=3,nh=2,rh=1,lh=10,ih=-2,ku=e=>e==="*";function oh(e,t){let n=e.split("/"),r=n.length;return n.some(ku)&&(r+=ih),t&&(r+=nh),n.filter(l=>!ku(l)).reduce((l,i)=>l+(eh.test(i)?th:i===""?rh:lh),r)}function sh(e,t){return e.length===t.length&&e.slice(0,-1).every((r,l)=>r===t[l])?e[e.length-1]-t[t.length-1]:0}function uh(e,t,n){let{routesMeta:r}=e,l={},i="/",o=[];for(let s=0;s<r.length;++s){let u=r[s],a=s===r.length-1,h=i==="/"?t:t.slice(i.length)||"/",c=ah({path:u.relativePath,caseSensitive:u.caseSensitLine truncated
|
*/function ir(){return ir=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},ir.apply(this,arguments)}var st;(function(e){e.Pop="POP",e.Push="PUSH",e.Replace="REPLACE"})(st||(st={}));const xu="popstate";function Yp(e){e===void 0&&(e={});function t(r,l){let{pathname:i,search:o,hash:s}=r.location;return ro("",{pathname:i,search:o,hash:s},l.state&&l.state.usr||null,l.state&&l.state.key||"default")}function n(r,l){return typeof l=="string"?l:gl(l)}return Zp(t,n,null,e)}function J(e,t){if(e===!1||e===null||typeof e>"u")throw new Error(t)}function rs(e,t){if(!e){typeof console<"u"&&console.warn(t);try{throw new Error(t)}catch{}}}function Xp(){return Math.random().toString(36).substr(2,8)}function wu(e,t){return{usr:e.state,key:e.key,idx:t}}function ro(e,t,n,r){return n===void 0&&(n=null),ir({pathname:typeof e=="string"?e:e.pathname,search:"",hash:""},typeof t=="string"?yn(t):t,{state:n,key:t&&t.key||r||Xp()})}function gl(e){let{pathname:t="/",search:n="",hash:r=""}=e;return n&&n!=="?"&&(t+=n.charAt(0)==="?"?n:"?"+n),r&&r!=="#"&&(t+=r.charAt(0)==="#"?r:"#"+r),t}function yn(e){let t={};if(e){let n=e.indexOf("#");n>=0&&(t.hash=e.substr(n),e=e.substr(0,n));let r=e.indexOf("?");r>=0&&(t.search=e.substr(r),e=e.substr(0,r)),e&&(t.pathname=e)}return t}function Zp(e,t,n,r){r===void 0&&(r={});let{window:l=document.defaultView,v5Compat:i=!1}=r,o=l.history,s=st.Pop,u=null,a=h();a==null&&(a=0,o.replaceState(ir({},o.state,{idx:a}),""));function h(){return(o.state||{idx:null}).idx}function f(){s=st.Pop;let C=h(),m=C==null?null:C-a;a=C,u&&u({action:s,location:w.location,delta:m})}function d(C,m){s=st.Push;let p=ro(w.location,C,m);a=h()+1;let v=wu(p,a),x=w.createHref(p);try{o.pushState(v,"",x)}catch(E){if(E instanceof DOMException&&E.name==="DataCloneError")throw E;l.location.assign(x)}i&&u&&u({action:s,location:w.location,delta:1})}function y(C,m){s=st.Replace;let p=ro(w.location,C,m);a=h();let v=wu(p,a),x=w.createHref(p);o.replaceState(v,"",x),i&&u&&u({action:s,location:w.location,delta:0})}function g(C){let m=l.location.origin!=="null"?l.location.origin:l.location.href,p=typeof C=="string"?C:gl(C);return p=p.replace(/ $/,"%20"),J(m,"No window.location.(origin|href) available to create URL for href: "+p),new URL(p,m)}let w={get action(){return s},get location(){return e(l,o)},listen(C){if(u)throw new Error("A history only accepts one active listener");return l.addEventListener(xu,f),u=C,()=>{l.removeEventListener(xu,f),u=null}},createHref(C){return t(l,C)},createURL:g,encodeLocation(C){let m=g(C);return{pathname:m.pathname,search:m.search,hash:m.hash}},push:d,replace:y,go(C){return o.go(C)}};return w}var Su;(function(e){e.data="data",e.deferred="deferred",e.redirect="redirect",e.error="error"})(Su||(Su={}));function qp(e,t,n){return n===void 0&&(n="/"),bp(e,t,n)}function bp(e,t,n,r){let l=typeof t=="string"?yn(t):t,i=ls(l.pathname||"/",n);if(i==null)return null;let o=tf(e);eh(o);let s=null;for(let u=0;s==null&&u<o.length;++u){let a=dh(i);s=ah(o[u],a)}return s}function tf(e,t,n,r){t===void 0&&(t=[]),n===void 0&&(n=[]),r===void 0&&(r="");let l=(i,o,s)=>{let u={relativePath:s===void 0?i.path||"":s,caseSensitive:i.caseSensitive===!0,childrenIndex:o,route:i};u.relativePath.startsWith("/")&&(J(u.relativePath.startsWith(r),'Absolute route path "'+u.relativePath+'" nested under path '+('"'+r+'" is not valid. An absolute child route path ')+"must start with the combined path of all its parent routes."),u.relativePath=u.relativePath.slice(r.length));let a=vt([r,u.relativePath]),h=n.concat(u);i.children&&i.children.length>0&&(J(i.index!==!0,"Index routes must not have child routes. Please remove "+('all child routes from route path "'+a+'".')),tf(i.children,t,h,a)),!(i.path==null&&!i.index)&&t.push({path:a,score:sh(a,i.index),routesMeta:h})};return e.forEach((i,o)=>{var s;if(i.path===""||!((s=i.path)!=null&&s.includes("?")))l(i,o);else for(let u of nf(i.path))l(i,o,u)}),t}function nf(e){let t=e.split("/");if(t.length===0)return[];let[n,...r]=t,l=n.endsWith("?"),i=n.replace(/\?$/,"");if(r.length===0)return l?[i,""]:[i];let o=nf(r.join("/")),s=[];return s.push(...o.map(u=>u===""?i:[i,u].join("/"))),l&&s.push(...o),s.map(u=>e.startsWith("/")&&u===""?"/":u)}function eh(e){e.sort((t,n)=>t.score!==n.score?n.score-t.score:uh(t.routesMeta.map(r=>r.childrenIndex),n.routesMeta.map(r=>r.childrenIndex)))}const th=/^:[\w-]+$/,nh=3,rh=2,lh=1,ih=10,oh=-2,ku=e=>e==="*";function sh(e,t){let n=e.split("/"),r=n.length;return n.some(ku)&&(r+=oh),t&&(r+=rh),n.filter(l=>!ku(l)).reduce((l,i)=>l+(th.test(i)?nh:i===""?lh:ih),r)}function uh(e,t){return e.length===t.length&&e.slice(0,-1).every((r,l)=>r===t[l])?e[e.length-1]-t[t.length-1]:0}function ah(e,t,n){let{routesMeta:r}=e,l={},i="/",o=[];for(let s=0;s<r.length;++s){let u=r[s],a=s===r.length-1,h=i==="/"?t:t.slice(i.length)||"/",f=ch({path:u.relativePath,caseSensitive:u.caseSensitLine truncated
|
||||||
* React Router v6.30.3
|
* React Router v6.30.3
|
||||||
*
|
*
|
||||||
* Copyright (c) Remix Software Inc.
|
* Copyright (c) Remix Software Inc.
|
||||||
@@ -55,7 +55,7 @@ Error generating stack: `+i.message+`
|
|||||||
* LICENSE.md file in the root directory of this source tree.
|
* LICENSE.md file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/function or(){return or=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},or.apply(this,arguments)}const ss=S.createContext(null),Sh=S.createContext(null),kt=S.createContext(null),Ol=S.createContext(null),be=S.createContext({outlet:null,matches:[],isDataRoute:!1}),rf=S.createContext(null);function kh(e,t){let{relative:n}=t===void 0?{}:t;xn()||J(!1);let{basename:r,navigator:l}=S.useContext(kt),{hash:i,pathname:o,search:s}=sf(e,{relative:n}),u=o;return r!=="/"&&(u=o==="/"?r:vt([r,o])),l.createHref({pathname:u,search:s,hash:i})}function xn(){return S.useContext(Ol)!=null}function dr(){return xn()||J(!1),S.useContext(Ol).location}function lf(e){S.useContext(kt).static||S.useLayoutEffect(e)}function us(){let{isDataRoute:e}=S.useContext(be);return e?Mh():Eh()}function Eh(){xn()||J(!1);let e=S.useContext(ss),{basename:t,future:n,navigator:r}=S.useContext(kt),{matches:l}=S.useContext(be),{pathname:i}=dr(),o=JSON.stringify(is(l,n.v7_relativeSplatPath)),s=S.useRef(!1);return lf(()=>{s.current=!0}),S.useCallback(function(a,h){if(h===void 0&&(h={}),!s.current)return;if(typeof a=="number"){r.go(a);return}let c=os(a,JSON.parse(o),i,h.relative==="path");e==null&&t!=="/"&&(c.pathname=c.pathname==="/"?t:vt([t,c.pathname])),(h.replace?r.replace:r.push)(c,h.state,h)},[t,r,o,i,e])}function of(){let{matches:e}=S.useContext(be),t=e[e.length-1];return t?t.params:{}}function sf(e,t){let{relative:n}=t===void 0?{}:t,{future:r}=S.useContext(kt),{matches:l}=S.useContext(be),{pathname:i}=dr(),o=JSON.stringify(is(l,r.v7_relativeSplatPath));return S.useMemo(()=>os(e,JSON.parse(o),i,n==="path"),[e,o,i,n])}function Ch(e,t){return Nh(e,t)}function Nh(e,t,n,r){xn()||J(!1);let{navigator:l}=S.useContext(kt),{matches:i}=S.useContext(be),o=i[i.length-1],s=o?o.params:{};o&&o.pathname;let u=o?o.pathnameBase:"/";o&&o.route;let a=dr(),h;if(t){var c;let N=typeof t=="string"?yn(t):t;u==="/"||(c=N.pathname)!=null&&c.startsWith(u)||J(!1),h=N}else h=a;let d=h.pathname||"/",y=d;if(u!=="/"){let N=u.replace(/^\//,"").split("/");y="/"+d.replace(/^\//,"").split("/").slice(N.length).join("/")}let g=Zp(e,{pathname:y}),w=Lh(g&&g.map(N=>Object.assign({},N,{params:Object.assign({},s,N.params),pathname:vt([u,l.encodeLocation?l.encodeLocation(N.pathname).pathname:N.pathname]),pathnameBase:N.pathnameBase==="/"?u:vt([u,l.encodeLocation?l.encodeLocation(N.pathnameBase).pathname:N.pathnameBase])})),i,n,r);return t&&w?S.createElement(Ol.Provider,{value:{location:or({pathname:"/",search:"",hash:"",state:null,key:"default"},h),navigationType:st.Pop}},w):w}function jh(){let e=Oh(),t=xh(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,l={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return S.createElement(S.Fragment,null,S.createElement("h2",null,"Unexpected Application Error!"),S.createElement("h3",{style:{fontStyle:"italic"}},t),n?S.createElement("pre",{style:l},n):null,null)}const _h=S.createElement(jh,null);class Ph extends S.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,n){return n.location!==t.location||n.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:n.error,location:n.location,revalidation:t.revalidation||n.revalidation}}componentDidCatch(t,n){console.error("React Router caught the following error during render",t,n)}render(){return this.state.error!==void 0?S.createElement(be.Provider,{value:this.props.routeContext},S.createElement(rf.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function Th(e){let{routeContext:t,match:n,children:r}=e,l=S.useContext(ss);return l&&l.static&&l.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(l.staticContext._deepestRenderedBoundaryId=n.route.id),S.createElement(be.Provider,{value:t},r)}function Lh(e,t,n,r){var l;if(t===void 0&&(t=[]),n===void 0&&(n=null),r===void 0&&(r=null),e==null){var i;if(!n)return null;if(n.errors)e=n.matches;else if((i=r)!=null&&i.v7_partialHydration&&t.length===0&&!n.initialized&&n.matches.length>0)e=n.matches;else return null}let o=e,s=(l=n)==null?void 0:l.errors;if(s!=null){let h=o.findIndex(c=>c.route.id&&(s==null?void 0:s[c.route.id])!==void 0);h>=0||J(!1),o=o.slice(0,Math.min(o.length,h+1))}let u=!1,a=-1;if(n&&r&&r.v7_partialHydration)for(let h=0;h<o.length;h++){let c=o[h];if((c.route.HydrateFallback||c.route.hydrateFallbackElement)&&(a=h),c.route.id){let{loaderData:d,errors:y}=n,g=c.route.loader&&d[c.route.id]===void 0&&(!y||y[c.route.id]===void 0);if(c.route.lazy||g){u=!0,a>=0?o=o.slice(0,a+1):o=[o[0]];break}}}return o.reduceRight((h,c,d)=>{let y,g=!1,w=null,N=null;n&&(y=s&&c.route.id?s[c.route.id]:void 0Line truncated
|
*/function or(){return or=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},or.apply(this,arguments)}const ss=S.createContext(null),kh=S.createContext(null),kt=S.createContext(null),Ol=S.createContext(null),be=S.createContext({outlet:null,matches:[],isDataRoute:!1}),lf=S.createContext(null);function Eh(e,t){let{relative:n}=t===void 0?{}:t;xn()||J(!1);let{basename:r,navigator:l}=S.useContext(kt),{hash:i,pathname:o,search:s}=uf(e,{relative:n}),u=o;return r!=="/"&&(u=o==="/"?r:vt([r,o])),l.createHref({pathname:u,search:s,hash:i})}function xn(){return S.useContext(Ol)!=null}function dr(){return xn()||J(!1),S.useContext(Ol).location}function of(e){S.useContext(kt).static||S.useLayoutEffect(e)}function us(){let{isDataRoute:e}=S.useContext(be);return e?Fh():Nh()}function Nh(){xn()||J(!1);let e=S.useContext(ss),{basename:t,future:n,navigator:r}=S.useContext(kt),{matches:l}=S.useContext(be),{pathname:i}=dr(),o=JSON.stringify(is(l,n.v7_relativeSplatPath)),s=S.useRef(!1);return of(()=>{s.current=!0}),S.useCallback(function(a,h){if(h===void 0&&(h={}),!s.current)return;if(typeof a=="number"){r.go(a);return}let f=os(a,JSON.parse(o),i,h.relative==="path");e==null&&t!=="/"&&(f.pathname=f.pathname==="/"?t:vt([t,f.pathname])),(h.replace?r.replace:r.push)(f,h.state,h)},[t,r,o,i,e])}function sf(){let{matches:e}=S.useContext(be),t=e[e.length-1];return t?t.params:{}}function uf(e,t){let{relative:n}=t===void 0?{}:t,{future:r}=S.useContext(kt),{matches:l}=S.useContext(be),{pathname:i}=dr(),o=JSON.stringify(is(l,r.v7_relativeSplatPath));return S.useMemo(()=>os(e,JSON.parse(o),i,n==="path"),[e,o,i,n])}function Ch(e,t){return jh(e,t)}function jh(e,t,n,r){xn()||J(!1);let{navigator:l}=S.useContext(kt),{matches:i}=S.useContext(be),o=i[i.length-1],s=o?o.params:{};o&&o.pathname;let u=o?o.pathnameBase:"/";o&&o.route;let a=dr(),h;if(t){var f;let C=typeof t=="string"?yn(t):t;u==="/"||(f=C.pathname)!=null&&f.startsWith(u)||J(!1),h=C}else h=a;let d=h.pathname||"/",y=d;if(u!=="/"){let C=u.replace(/^\//,"").split("/");y="/"+d.replace(/^\//,"").split("/").slice(C.length).join("/")}let g=qp(e,{pathname:y}),w=Rh(g&&g.map(C=>Object.assign({},C,{params:Object.assign({},s,C.params),pathname:vt([u,l.encodeLocation?l.encodeLocation(C.pathname).pathname:C.pathname]),pathnameBase:C.pathnameBase==="/"?u:vt([u,l.encodeLocation?l.encodeLocation(C.pathnameBase).pathname:C.pathnameBase])})),i,n,r);return t&&w?S.createElement(Ol.Provider,{value:{location:or({pathname:"/",search:"",hash:"",state:null,key:"default"},h),navigationType:st.Pop}},w):w}function _h(){let e=Mh(),t=wh(e)?e.status+" "+e.statusText:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,l={padding:"0.5rem",backgroundColor:"rgba(200,200,200, 0.5)"};return S.createElement(S.Fragment,null,S.createElement("h2",null,"Unexpected Application Error!"),S.createElement("h3",{style:{fontStyle:"italic"}},t),n?S.createElement("pre",{style:l},n):null,null)}const Ph=S.createElement(_h,null);class Th extends S.Component{constructor(t){super(t),this.state={location:t.location,revalidation:t.revalidation,error:t.error}}static getDerivedStateFromError(t){return{error:t}}static getDerivedStateFromProps(t,n){return n.location!==t.location||n.revalidation!=="idle"&&t.revalidation==="idle"?{error:t.error,location:t.location,revalidation:t.revalidation}:{error:t.error!==void 0?t.error:n.error,location:n.location,revalidation:t.revalidation||n.revalidation}}componentDidCatch(t,n){console.error("React Router caught the following error during render",t,n)}render(){return this.state.error!==void 0?S.createElement(be.Provider,{value:this.props.routeContext},S.createElement(lf.Provider,{value:this.state.error,children:this.props.component})):this.props.children}}function Lh(e){let{routeContext:t,match:n,children:r}=e,l=S.useContext(ss);return l&&l.static&&l.staticContext&&(n.route.errorElement||n.route.ErrorBoundary)&&(l.staticContext._deepestRenderedBoundaryId=n.route.id),S.createElement(be.Provider,{value:t},r)}function Rh(e,t,n,r){var l;if(t===void 0&&(t=[]),n===void 0&&(n=null),r===void 0&&(r=null),e==null){var i;if(!n)return null;if(n.errors)e=n.matches;else if((i=r)!=null&&i.v7_partialHydration&&t.length===0&&!n.initialized&&n.matches.length>0)e=n.matches;else return null}let o=e,s=(l=n)==null?void 0:l.errors;if(s!=null){let h=o.findIndex(f=>f.route.id&&(s==null?void 0:s[f.route.id])!==void 0);h>=0||J(!1),o=o.slice(0,Math.min(o.length,h+1))}let u=!1,a=-1;if(n&&r&&r.v7_partialHydration)for(let h=0;h<o.length;h++){let f=o[h];if((f.route.HydrateFallback||f.route.hydrateFallbackElement)&&(a=h),f.route.id){let{loaderData:d,errors:y}=n,g=f.route.loader&&d[f.route.id]===void 0&&(!y||y[f.route.id]===void 0);if(f.route.lazy||g){u=!0,a>=0?o=o.slice(0,a+1):o=[o[0]];break}}}return o.reduceRight((h,f,d)=>{let y,g=!1,w=null,C=null;n&&(y=s&&f.route.id?s[f.route.id]:void 0Line truncated
|
||||||
* React Router DOM v6.30.3
|
* React Router DOM v6.30.3
|
||||||
*
|
*
|
||||||
* Copyright (c) Remix Software Inc.
|
* Copyright (c) Remix Software Inc.
|
||||||
@@ -64,4 +64,4 @@ Error generating stack: `+i.message+`
|
|||||||
* LICENSE.md file in the root directory of this source tree.
|
* LICENSE.md file in the root directory of this source tree.
|
||||||
*
|
*
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/function io(){return io=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},io.apply(this,arguments)}function Vh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,i;for(i=0;i<r.length;i++)l=r[i],!(t.indexOf(l)>=0)&&(n[l]=e[l]);return n}function Bh(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Wh(e,t){return e.button===0&&(!t||t==="_self")&&!Bh(e)}const Hh=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],Qh="6";try{window.__reactRouterVersion=Qh}catch{}const Kh="startTransition",Nu=Ff[Kh];function Gh(e){let{basename:t,children:n,future:r,window:l}=e,i=S.useRef();i.current==null&&(i.current=Jp({window:l,v5Compat:!0}));let o=i.current,[s,u]=S.useState({action:o.action,location:o.location}),{v7_startTransition:a}=r||{},h=S.useCallback(c=>{a&&Nu?Nu(()=>u(c)):u(c)},[u,a]);return S.useLayoutEffect(()=>o.listen(h),[o,h]),S.useEffect(()=>Dh(r),[r]),S.createElement(Uh,{basename:t,children:n,location:s.location,navigationType:s.action,navigator:o,future:r})}const Jh=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",Yh=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,ff=S.forwardRef(function(t,n){let{onClick:r,relative:l,reloadDocument:i,replace:o,state:s,target:u,to:a,preventScrollReset:h,viewTransition:c}=t,d=Vh(t,Hh),{basename:y}=S.useContext(kt),g,w=!1;if(typeof a=="string"&&Yh.test(a)&&(g=a,Jh))try{let v=new URL(window.location.href),x=a.startsWith("//")?new URL(v.protocol+a):new URL(a),E=ls(x.pathname,y);x.origin===v.origin&&E!=null?a=E+x.search+x.hash:w=!0}catch{}let N=kh(a,{relative:l}),m=Xh(a,{replace:o,state:s,target:u,preventScrollReset:h,relative:l,viewTransition:c});function f(v){r&&r(v),v.defaultPrevented||m(v)}return S.createElement("a",io({},d,{href:g||N,onClick:w||i?r:f,ref:n,target:u}))});var ju;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(ju||(ju={}));var _u;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(_u||(_u={}));function Xh(e,t){let{target:n,replace:r,state:l,preventScrollReset:i,relative:o,viewTransition:s}=t===void 0?{}:t,u=us(),a=dr(),h=sf(e,{relative:o});return S.useCallback(c=>{if(Wh(c,n)){c.preventDefault();let d=r!==void 0?r:gl(a)===gl(h);u(e,{replace:d,state:l,preventScrollReset:i,relative:o,viewTransition:s})}},[a,u,h,r,l,n,e,i,o,s])}async function as(e){const t=await fetch(e,{headers:{Accept:"application/json"}});if(!t.ok)throw new Error(`request failed: ${t.status} ${t.statusText} (${e})`);return await t.json()}function Zh(){return as("/api/runs")}function qh(e){return as(`/api/runs/${encodeURIComponent(e)}`)}function bh(e,t){return as(`/api/runs/${encodeURIComponent(e)}/steps/${t}`)}function Pu(e,t){return`/api/runs/${encodeURIComponent(e)}/screenshots/${encodeURIComponent(t)}`}function em(e,t){S.useEffect(()=>{if(typeof EventSource>"u")return;const n=new EventSource("/api/events"),r=()=>{t()};return n.addEventListener(e,r),()=>{n.removeEventListener(e,r),n.close()}},[e,t])}function tm(e){if(!e)return"-";if(e<1e3)return`${e}ms`;const t=e/1e3;if(t<60)return`${t.toFixed(1)}s`;const n=Math.floor(t/60),r=Math.round(t-n*60);return`${n}m${r}s`}function nm(e){const t=new Date(e);return Number.isNaN(t.getTime())?e:t.toLocaleString()}function rm(){const[e,t]=S.useState(null),[n,r]=S.useState(null),l=S.useCallback(()=>{Zh().then(t).catch(i=>{r(i instanceof Error?i.message:String(i))})},[]);return S.useEffect(()=>{l()},[l]),em("runs.changed",l),n?p.jsxs("div",{className:"status-block status-error",children:["failed to load runs: ",n]}):e?e.length===0?p.jsx("div",{className:"status-block",children:"no runs yet. run `sanderling test` to produce some."}):p.jsxs("table",{className:"run-table",children:[p.jsx("thead",{children:p.jsxs("tr",{children:[p.jsx("th",{children:"started"}),p.jsx("th",{children:"spec"}),p.jsx("th",{children:"seed"}),p.jsx("th",{children:"platform"}),p.jsx("th",{children:"duration"}),p.jsx("th",{children:"steps"}),p.jsx("th",{children:"violations"}),p.jsx("th",{})]})}),p.jsx("tbody",{children:e.map(i=>p.jsxs("tr",{children:[p.jsx("td",{children:p.jsx(ff,{to:`/runs/${i.id}`,children:nm(i.started_at)})}),p.jsx("td",{children:i.spec_path}),p.jsx("td",{children:i.seed}),p.jsx("td",{children:i.platform}),p.jsx("td",{children:tm(i.duration_millis)}),p.jsx("td",{children:i.step_count}),p.jsx("td",{children:i.violation_count>0?p.jsx("span",{className:"chip chip-violation",children:i.violation_count}):"0"}),p.jsx("td",{children:i.in_progress?p.jsx("span",{className:"chip chip-progress",children:"in progress"}):null})]},i.id))})]}):p.jsx("div",{className:"status-block",children:"loading runs..."})}const lm=["id","text","textPrefix","dLine truncated
|
*/function io(){return io=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},io.apply(this,arguments)}function Bh(e,t){if(e==null)return{};var n={},r=Object.keys(e),l,i;for(i=0;i<r.length;i++)l=r[i],!(t.indexOf(l)>=0)&&(n[l]=e[l]);return n}function Wh(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function Hh(e,t){return e.button===0&&(!t||t==="_self")&&!Wh(e)}const Qh=["onClick","relative","reloadDocument","replace","state","target","to","preventScrollReset","viewTransition"],Kh="6";try{window.__reactRouterVersion=Kh}catch{}const Gh="startTransition",Cu=Df[Gh];function Jh(e){let{basename:t,children:n,future:r,window:l}=e,i=S.useRef();i.current==null&&(i.current=Yp({window:l,v5Compat:!0}));let o=i.current,[s,u]=S.useState({action:o.action,location:o.location}),{v7_startTransition:a}=r||{},h=S.useCallback(f=>{a&&Cu?Cu(()=>u(f)):u(f)},[u,a]);return S.useLayoutEffect(()=>o.listen(h),[o,h]),S.useEffect(()=>$h(r),[r]),S.createElement(Ah,{basename:t,children:n,location:s.location,navigationType:s.action,navigator:o,future:r})}const Yh=typeof window<"u"&&typeof window.document<"u"&&typeof window.document.createElement<"u",Xh=/^(?:[a-z][a-z0-9+.-]*:|\/\/)/i,df=S.forwardRef(function(t,n){let{onClick:r,relative:l,reloadDocument:i,replace:o,state:s,target:u,to:a,preventScrollReset:h,viewTransition:f}=t,d=Bh(t,Qh),{basename:y}=S.useContext(kt),g,w=!1;if(typeof a=="string"&&Xh.test(a)&&(g=a,Yh))try{let v=new URL(window.location.href),x=a.startsWith("//")?new URL(v.protocol+a):new URL(a),E=ls(x.pathname,y);x.origin===v.origin&&E!=null?a=E+x.search+x.hash:w=!0}catch{}let C=Eh(a,{relative:l}),m=Zh(a,{replace:o,state:s,target:u,preventScrollReset:h,relative:l,viewTransition:f});function p(v){r&&r(v),v.defaultPrevented||m(v)}return S.createElement("a",io({},d,{href:g||C,onClick:w||i?r:p,ref:n,target:u}))});var ju;(function(e){e.UseScrollRestoration="useScrollRestoration",e.UseSubmit="useSubmit",e.UseSubmitFetcher="useSubmitFetcher",e.UseFetcher="useFetcher",e.useViewTransitionState="useViewTransitionState"})(ju||(ju={}));var _u;(function(e){e.UseFetcher="useFetcher",e.UseFetchers="useFetchers",e.UseScrollRestoration="useScrollRestoration"})(_u||(_u={}));function Zh(e,t){let{target:n,replace:r,state:l,preventScrollReset:i,relative:o,viewTransition:s}=t===void 0?{}:t,u=us(),a=dr(),h=uf(e,{relative:o});return S.useCallback(f=>{if(Hh(f,n)){f.preventDefault();let d=r!==void 0?r:gl(a)===gl(h);u(e,{replace:d,state:l,preventScrollReset:i,relative:o,viewTransition:s})}},[a,u,h,r,l,n,e,i,o,s])}async function as(e){const t=await fetch(e,{headers:{Accept:"application/json"}});if(!t.ok)throw new Error(`request failed: ${t.status} ${t.statusText} (${e})`);return await t.json()}function qh(){return as("/api/runs")}function bh(e){return as(`/api/runs/${encodeURIComponent(e)}`)}function em(e,t){return as(`/api/runs/${encodeURIComponent(e)}/steps/${t}`)}function Pu(e,t){return`/api/runs/${encodeURIComponent(e)}/screenshots/${encodeURIComponent(t)}`}function tm(e,t){S.useEffect(()=>{if(typeof EventSource>"u")return;const n=new EventSource("/api/events"),r=()=>{t()};return n.addEventListener(e,r),()=>{n.removeEventListener(e,r),n.close()}},[e,t])}function nm(e){if(!e)return"-";if(e<1e3)return`${e}ms`;const t=e/1e3;if(t<60)return`${t.toFixed(1)}s`;const n=Math.floor(t/60),r=Math.round(t-n*60);return`${n}m${r}s`}function rm(e){const t=new Date(e);return Number.isNaN(t.getTime())?e:t.toLocaleString()}function lm(){const[e,t]=S.useState(null),[n,r]=S.useState(null),l=S.useCallback(()=>{qh().then(t).catch(i=>{r(i instanceof Error?i.message:String(i))})},[]);return S.useEffect(()=>{l()},[l]),tm("runs.changed",l),n?c.jsxs("div",{className:"status-block status-error",children:["failed to load runs: ",n]}):e?e.length===0?c.jsx("div",{className:"status-block",children:"no runs yet. run `sanderling test` to produce some."}):c.jsxs("table",{className:"run-table",children:[c.jsx("thead",{children:c.jsxs("tr",{children:[c.jsx("th",{children:"started"}),c.jsx("th",{children:"spec"}),c.jsx("th",{children:"seed"}),c.jsx("th",{children:"platform"}),c.jsx("th",{children:"duration"}),c.jsx("th",{children:"steps"}),c.jsx("th",{children:"violations"}),c.jsx("th",{})]})}),c.jsx("tbody",{children:e.map(i=>c.jsxs("tr",{children:[c.jsx("td",{children:c.jsx(df,{to:`/runs/${i.id}`,children:rm(i.started_at)})}),c.jsx("td",{children:i.spec_path}),c.jsx("td",{children:i.seed}),c.jsx("td",{children:i.platform}),c.jsx("td",{children:nm(i.duration_millis)}),c.jsx("td",{children:i.step_count}),c.jsx("td",{children:i.violation_count>0?c.jsx("span",{className:"chip chip-violation",children:i.violation_count}):"0"}),c.jsx("td",{children:i.in_progress?c.jsx("span",{className:"chip chip-progress",children:"in progress"}):null})]},i.id))})]}):c.jsx("div",{className:"status-block",children:"loading runs..."})}const im=["id","text","textPrefix","dLine truncated
|
||||||
Vendored
+2
-2
@@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>sanderling inspect</title>
|
<title>sanderling inspect</title>
|
||||||
<script type="module" crossorigin src="/assets/index-DAhEzdrG.js"></script>
|
<script type="module" crossorigin src="/assets/index-CEStFXHZ.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-BSJUf7yI.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-BV6z-_9-.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"react-router-dom": "^6.26.2"
|
"react-router-dom": "^6.26.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/bun": "^1.3.13",
|
||||||
"@types/react": "^18.3.5",
|
"@types/react": "^18.3.5",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@vitejs/plugin-react": "^4.3.1",
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { describe, it, expect } from "bun:test";
|
||||||
|
import { screenshotUrl } from "../api";
|
||||||
|
|
||||||
|
describe("screenshotUrl", () => {
|
||||||
|
it("encodes runId and name", () => {
|
||||||
|
expect(screenshotUrl("run-1", "step-00001.png")).toBe(
|
||||||
|
"/api/runs/run-1/screenshots/step-00001.png",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
.hierarchy-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-filter {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-table-wrap {
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 480px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-table th {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
background: var(--surface);
|
||||||
|
text-align: left;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-weight: normal;
|
||||||
|
color: var(--text-muted);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-table td {
|
||||||
|
padding: 2px 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-table tr:hover td {
|
||||||
|
background: var(--surface-elevated);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-tag {
|
||||||
|
color: var(--text-muted);
|
||||||
|
max-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-id {
|
||||||
|
color: var(--text-primary);
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-text {
|
||||||
|
max-width: 320px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-bounds {
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-empty {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hierarchy-count {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
import type { HierarchyElement, Hierarchy } from "../types";
|
||||||
|
import "./HierarchyPanel.css";
|
||||||
|
|
||||||
|
interface HierarchyPanelProps {
|
||||||
|
hierarchy?: Hierarchy;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HierarchyPanel({ hierarchy }: HierarchyPanelProps): JSX.Element {
|
||||||
|
const [filter, setFilter] = useState("");
|
||||||
|
const elements = hierarchy?.elements ?? [];
|
||||||
|
|
||||||
|
const rows = useMemo(() => {
|
||||||
|
const lower = filter.trim().toLowerCase();
|
||||||
|
if (!lower) return elements;
|
||||||
|
return elements.filter((element) => matches(element, lower));
|
||||||
|
}, [elements, filter]);
|
||||||
|
|
||||||
|
if (elements.length === 0) {
|
||||||
|
return <div className="hierarchy-empty">no hierarchy captured</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="hierarchy-panel">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="hierarchy-filter"
|
||||||
|
placeholder="filter by id, text, desc, class, tag..."
|
||||||
|
value={filter}
|
||||||
|
onChange={(e) => setFilter(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="hierarchy-table-wrap">
|
||||||
|
<table className="hierarchy-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>tag/class</th>
|
||||||
|
<th>id</th>
|
||||||
|
<th>text/desc</th>
|
||||||
|
<th>bounds</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((element, index) => (
|
||||||
|
<HierarchyRow key={index} element={element} />
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div className="hierarchy-count">
|
||||||
|
{rows.length} of {elements.length}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function HierarchyRow({ element }: { element: HierarchyElement }): JSX.Element {
|
||||||
|
const tag = element.tag ?? element.class ?? "";
|
||||||
|
const text = (element.text ?? element.description ?? "").slice(0, 80);
|
||||||
|
const bounds = element.bounds;
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td className="hierarchy-tag">{tag}</td>
|
||||||
|
<td className="hierarchy-id">{element.resourceId ?? ""}</td>
|
||||||
|
<td className="hierarchy-text">{text}</td>
|
||||||
|
<td className="hierarchy-bounds">
|
||||||
|
[{bounds.left},{bounds.top},{bounds.right},{bounds.bottom}]
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function matches(element: HierarchyElement, needle: string): boolean {
|
||||||
|
const fields = [
|
||||||
|
element.resourceId,
|
||||||
|
element.text,
|
||||||
|
element.description,
|
||||||
|
element.class,
|
||||||
|
element.tag,
|
||||||
|
];
|
||||||
|
for (const value of fields) {
|
||||||
|
if (value && value.toLowerCase().includes(needle)) return true;
|
||||||
|
}
|
||||||
|
if (element.attributes) {
|
||||||
|
for (const value of Object.values(element.attributes)) {
|
||||||
|
if (value.toLowerCase().includes(needle)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { Link } from "react-router-dom";
|
|||||||
import { getRun, getStep, screenshotUrl } from "../api";
|
import { getRun, getStep, screenshotUrl } from "../api";
|
||||||
import type { Run, Step } from "../types";
|
import type { Run, Step } from "../types";
|
||||||
import ActionList from "../panels/ActionList";
|
import ActionList from "../panels/ActionList";
|
||||||
|
import HierarchyPanel from "../panels/HierarchyPanel";
|
||||||
import Screenshot from "../panels/Screenshot";
|
import Screenshot from "../panels/Screenshot";
|
||||||
import SnapshotTable from "../panels/SnapshotTable";
|
import SnapshotTable from "../panels/SnapshotTable";
|
||||||
import ViolationsPanel from "../panels/ViolationsPanel";
|
import ViolationsPanel from "../panels/ViolationsPanel";
|
||||||
@@ -135,6 +136,11 @@ export default function RunDetail() {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "hierarchy",
|
||||||
|
label: "Hierarchy",
|
||||||
|
content: <HierarchyPanel hierarchy={currentStep?.hierarchy} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "properties",
|
id: "properties",
|
||||||
label: "Properties",
|
label: "Properties",
|
||||||
@@ -186,6 +192,11 @@ export default function RunDetail() {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "hierarchy",
|
||||||
|
label: "Hierarchy",
|
||||||
|
content: <HierarchyPanel hierarchy={nextStep?.hierarchy ?? currentStep?.hierarchy} />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "properties",
|
id: "properties",
|
||||||
label: "Properties",
|
label: "Properties",
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export interface HierarchyElement {
|
|||||||
text?: string;
|
text?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
|
tag?: string;
|
||||||
package?: string;
|
package?: string;
|
||||||
clickable?: boolean;
|
clickable?: boolean;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
@@ -57,6 +58,7 @@ export interface HierarchyElement {
|
|||||||
focused?: boolean;
|
focused?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
bounds: BoundsRect;
|
bounds: BoundsRect;
|
||||||
|
attributes?: Record<string, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Hierarchy {
|
export interface Hierarchy {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"types": ["vite/client"]
|
"types": ["vite/client", "bun"]
|
||||||
},
|
},
|
||||||
"include": ["src", "vite.config.ts"]
|
"include": ["src", "vite.config.ts"]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
package bundler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
esbuild "github.com/evanw/esbuild/pkg/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WebOptions configures BundleWeb. WebRuntimeFile points at
|
||||||
|
// pkg/spec/src/web-runtime.ts (resolved via the same upward search the regular
|
||||||
|
// Bundle uses for @sanderling/spec). When empty, BundleWeb expects callers to
|
||||||
|
// have set Aliases such that "@sanderling/spec/web-runtime" resolves.
|
||||||
|
type WebOptions struct {
|
||||||
|
EntryFile string
|
||||||
|
WebRuntimeFile string
|
||||||
|
Defines map[string]string
|
||||||
|
Aliases map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// BundleWeb compiles the user spec together with the V8-side runtime into a
|
||||||
|
// single IIFE that, on evaluation, installs `globalThis.__sanderling__` and
|
||||||
|
// the `__sanderlingExtractors__` / `__sanderlingNextAction__` globals. The
|
||||||
|
// host injects the result via Page.AddScriptToEvaluateOnNewDocument.
|
||||||
|
func BundleWeb(options WebOptions) (Result, error) {
|
||||||
|
if options.EntryFile == "" {
|
||||||
|
return Result{}, errors.New("EntryFile is required")
|
||||||
|
}
|
||||||
|
if options.WebRuntimeFile == "" {
|
||||||
|
return Result{}, errors.New("WebRuntimeFile is required")
|
||||||
|
}
|
||||||
|
if _, err := filepath.Abs(options.EntryFile); err != nil {
|
||||||
|
return Result{}, fmt.Errorf("entry path: %w", err)
|
||||||
|
}
|
||||||
|
runtimeAbs, err := filepath.Abs(options.WebRuntimeFile)
|
||||||
|
if err != nil {
|
||||||
|
return Result{}, fmt.Errorf("runtime path: %w", err)
|
||||||
|
}
|
||||||
|
specAbs, err := filepath.Abs(options.EntryFile)
|
||||||
|
if err != nil {
|
||||||
|
return Result{}, fmt.Errorf("entry path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defines := map[string]string{}
|
||||||
|
for key, value := range options.Defines {
|
||||||
|
defines["process.env."+key] = quoteJSString(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
stdinContents := fmt.Sprintf(`import %q;
|
||||||
|
import %q;
|
||||||
|
`, runtimeAbs, specAbs)
|
||||||
|
|
||||||
|
output := esbuild.Build(esbuild.BuildOptions{
|
||||||
|
Stdin: &esbuild.StdinOptions{
|
||||||
|
Contents: stdinContents,
|
||||||
|
ResolveDir: filepath.Dir(specAbs),
|
||||||
|
Loader: esbuild.LoaderTS,
|
||||||
|
},
|
||||||
|
Bundle: true,
|
||||||
|
Format: esbuild.FormatIIFE,
|
||||||
|
Target: esbuild.ES2020,
|
||||||
|
Platform: esbuild.PlatformBrowser,
|
||||||
|
Define: defines,
|
||||||
|
Alias: options.Aliases,
|
||||||
|
Write: false,
|
||||||
|
LogLevel: esbuild.LogLevelSilent,
|
||||||
|
})
|
||||||
|
|
||||||
|
if len(output.Errors) > 0 {
|
||||||
|
var messages []string
|
||||||
|
for _, message := range output.Errors {
|
||||||
|
messages = append(messages, message.Text)
|
||||||
|
}
|
||||||
|
return Result{}, fmt.Errorf("web bundle failed: %s", strings.Join(messages, "; "))
|
||||||
|
}
|
||||||
|
if len(output.OutputFiles) == 0 {
|
||||||
|
return Result{}, errors.New("web bundle produced no output files")
|
||||||
|
}
|
||||||
|
|
||||||
|
javascript := output.OutputFiles[0].Contents
|
||||||
|
sum := sha256.Sum256(javascript)
|
||||||
|
return Result{
|
||||||
|
JavaScript: javascript,
|
||||||
|
SHA256: hex.EncodeToString(sum[:]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func quoteJSString(value string) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteByte('"')
|
||||||
|
for index := 0; index < len(value); index++ {
|
||||||
|
c := value[index]
|
||||||
|
switch c {
|
||||||
|
case '"', '\\':
|
||||||
|
builder.WriteByte('\\')
|
||||||
|
builder.WriteByte(c)
|
||||||
|
case '\n':
|
||||||
|
builder.WriteString("\\n")
|
||||||
|
case '\r':
|
||||||
|
builder.WriteString("\\r")
|
||||||
|
case '\t':
|
||||||
|
builder.WriteString("\\t")
|
||||||
|
default:
|
||||||
|
builder.WriteByte(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.WriteByte('"')
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
package bundler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const fakeRuntime = `
|
||||||
|
(globalThis as Record<string, unknown>).__sanderling__ = {
|
||||||
|
extract: (g: () => unknown) => ({ current: g(), previous: undefined }),
|
||||||
|
always: () => ({ __sanderlingFormula: true }),
|
||||||
|
now: () => ({ __sanderlingFormula: true }),
|
||||||
|
next: () => ({ __sanderlingFormula: true }),
|
||||||
|
eventually: () => ({ __sanderlingFormula: true }),
|
||||||
|
actions: (g: () => unknown) => ({ __sanderlingActionGenerator: true, generate: g }),
|
||||||
|
weighted: () => ({ __sanderlingActionGenerator: true }),
|
||||||
|
from: () => ({ generate: () => null }),
|
||||||
|
tap: () => null,
|
||||||
|
inputText: () => null,
|
||||||
|
swipe: () => null,
|
||||||
|
pressKey: () => null,
|
||||||
|
wait: () => null,
|
||||||
|
taps: {},
|
||||||
|
swipes: {},
|
||||||
|
waitOnce: {},
|
||||||
|
pressKeys: {},
|
||||||
|
};
|
||||||
|
(globalThis as Record<string, unknown>).__sanderlingExtractors__ = () => ({});
|
||||||
|
(globalThis as Record<string, unknown>).__sanderlingNextAction__ = () => null;
|
||||||
|
export {};
|
||||||
|
`
|
||||||
|
|
||||||
|
const fakeSpec = `
|
||||||
|
const handle = (globalThis as { __sanderling__: { extract: (g: () => unknown) => unknown } }).__sanderling__.extract(() => 42);
|
||||||
|
(globalThis as { actions?: unknown }).actions = handle;
|
||||||
|
export {};
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestBundleWeb_RegistersExpectedGlobals(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
runtimePath := filepath.Join(directory, "web-runtime.ts")
|
||||||
|
specPath := filepath.Join(directory, "spec.ts")
|
||||||
|
if err := os.WriteFile(runtimePath, []byte(fakeRuntime), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(specPath, []byte(fakeSpec), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := BundleWeb(WebOptions{
|
||||||
|
EntryFile: specPath,
|
||||||
|
WebRuntimeFile: runtimePath,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BundleWeb: %v", err)
|
||||||
|
}
|
||||||
|
source := string(result.JavaScript)
|
||||||
|
for _, expected := range []string{
|
||||||
|
"__sanderlingExtractors__",
|
||||||
|
"__sanderlingNextAction__",
|
||||||
|
"__sanderling__",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(source, expected) {
|
||||||
|
t.Errorf("bundle missing %q\nsource head:\n%s", expected, head(source, 500))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(strings.TrimSpace(source), "(()") {
|
||||||
|
t.Errorf("expected IIFE format, head:\n%s", head(source, 200))
|
||||||
|
}
|
||||||
|
if result.SHA256 == "" {
|
||||||
|
t.Errorf("expected non-empty sha256")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBundleWeb_RejectsMissingEntry(t *testing.T) {
|
||||||
|
if _, err := BundleWeb(WebOptions{}); err == nil {
|
||||||
|
t.Error("expected error for empty options")
|
||||||
|
}
|
||||||
|
if _, err := BundleWeb(WebOptions{EntryFile: "x.ts"}); err == nil {
|
||||||
|
t.Error("expected error for missing runtime")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBundleWeb_IsDeterministic builds the same source repeatedly with multiple
|
||||||
|
// Defines entries and asserts the SHA matches. This guards against any future
|
||||||
|
// change that introduces map-iteration ordering into the bundle output.
|
||||||
|
func TestBundleWeb_IsDeterministic(t *testing.T) {
|
||||||
|
directory := t.TempDir()
|
||||||
|
runtimePath := filepath.Join(directory, "web-runtime.ts")
|
||||||
|
specPath := filepath.Join(directory, "spec.ts")
|
||||||
|
if err := os.WriteFile(runtimePath, []byte(fakeRuntime), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(specPath, []byte(fakeSpec), 0o644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
options := WebOptions{
|
||||||
|
EntryFile: specPath,
|
||||||
|
WebRuntimeFile: runtimePath,
|
||||||
|
Defines: map[string]string{
|
||||||
|
"API_BASE": "https://example.com",
|
||||||
|
"FEATURE": "true",
|
||||||
|
"VERSION": "1.2.3",
|
||||||
|
"TENANT": "alpha",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
first, err := BundleWeb(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("first build: %v", err)
|
||||||
|
}
|
||||||
|
for attempt := range 10 {
|
||||||
|
repeat, err := BundleWeb(options)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("attempt %d: %v", attempt, err)
|
||||||
|
}
|
||||||
|
if repeat.SHA256 != first.SHA256 {
|
||||||
|
t.Fatalf("attempt %d: SHA changed (%s vs %s)", attempt, repeat.SHA256, first.SHA256)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func head(text string, max int) string {
|
||||||
|
if len(text) <= max {
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
return text[:max] + "..."
|
||||||
|
}
|
||||||
@@ -4,14 +4,17 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/chromedp/cdproto/input"
|
"github.com/chromedp/cdproto/input"
|
||||||
"github.com/chromedp/cdproto/network"
|
"github.com/chromedp/cdproto/network"
|
||||||
|
"github.com/chromedp/cdproto/page"
|
||||||
"github.com/chromedp/cdproto/runtime"
|
"github.com/chromedp/cdproto/runtime"
|
||||||
"github.com/chromedp/chromedp"
|
"github.com/chromedp/chromedp"
|
||||||
|
"github.com/chromedp/chromedp/kb"
|
||||||
|
|
||||||
"github.com/priyanshujain/sanderling/internal/driver"
|
"github.com/priyanshujain/sanderling/internal/driver"
|
||||||
)
|
)
|
||||||
@@ -126,9 +129,16 @@ func (d *Driver) Tap(_ context.Context, x, y int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) TapSelector(_ context.Context, selector string) error {
|
func (d *Driver) TapSelector(_ context.Context, selector string) error {
|
||||||
return chromedp.Run(d.tabCtx,
|
target, isXPath, err := TranslateStringSelector(selector)
|
||||||
chromedp.Click(selector, chromedp.NodeVisible),
|
if err != nil {
|
||||||
)
|
// Fall back to passing the string straight through; chromedp will
|
||||||
|
// reject it loudly if it isn't a valid CSS selector.
|
||||||
|
target = selector
|
||||||
|
}
|
||||||
|
if isXPath {
|
||||||
|
return chromedp.Run(d.tabCtx, chromedp.Click(target, chromedp.NodeVisible, chromedp.BySearch))
|
||||||
|
}
|
||||||
|
return chromedp.Run(d.tabCtx, chromedp.Click(target, chromedp.NodeVisible))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) InputText(_ context.Context, text string) error {
|
func (d *Driver) InputText(_ context.Context, text string) error {
|
||||||
@@ -180,15 +190,17 @@ func (d *Driver) PressKey(_ context.Context, key string) error {
|
|||||||
return chromedp.Run(d.tabCtx, chromedp.KeyEvent(k))
|
return chromedp.Run(d.tabCtx, chromedp.KeyEvent(k))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keyMap covers the keys web specs may emit (enter/tab/escape/arrows).
|
||||||
|
// "back"/"home" are intentionally absent: backspace/NUL have no navigation
|
||||||
|
// semantics in a browser, and the V8 action mix already excludes them.
|
||||||
var keyMap = map[string]string{
|
var keyMap = map[string]string{
|
||||||
"back": "\b",
|
"enter": kb.Enter,
|
||||||
"home": "\x00",
|
"tab": kb.Tab,
|
||||||
"enter": "\r",
|
"escape": kb.Escape,
|
||||||
"tab": "\t",
|
"up": kb.ArrowUp,
|
||||||
"up": "\x26",
|
"down": kb.ArrowDown,
|
||||||
"down": "\x28",
|
"left": kb.ArrowLeft,
|
||||||
"left": "\x25",
|
"right": kb.ArrowRight,
|
||||||
"right": "\x27",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) Hierarchy(_ context.Context) (string, error) {
|
func (d *Driver) Hierarchy(_ context.Context) (string, error) {
|
||||||
@@ -206,7 +218,10 @@ func (d *Driver) Hierarchy(_ context.Context) (string, error) {
|
|||||||
if (el.id) attrs['resource-id'] = el.id;
|
if (el.id) attrs['resource-id'] = el.id;
|
||||||
const label = el.getAttribute('aria-label') || el.getAttribute('alt') || el.getAttribute('title') || '';
|
const label = el.getAttribute('aria-label') || el.getAttribute('alt') || el.getAttribute('title') || '';
|
||||||
if (label) attrs['content-desc'] = label;
|
if (label) attrs['content-desc'] = label;
|
||||||
if (el.tagName) attrs['class'] = el.tagName.toLowerCase();
|
if (el.tagName) attrs['tag'] = el.tagName.toLowerCase();
|
||||||
|
if (el.className && typeof el.className === 'string' && el.className.trim()) {
|
||||||
|
attrs['class'] = el.className.trim();
|
||||||
|
}
|
||||||
if (isRoot) attrs['sanderling-screen'] = route;
|
if (isRoot) attrs['sanderling-screen'] = route;
|
||||||
const isClickable = !!(el.onclick || el.tagName === 'A' || el.tagName === 'BUTTON' ||
|
const isClickable = !!(el.onclick || el.tagName === 'A' || el.tagName === 'BUTTON' ||
|
||||||
el.tagName === 'INPUT' || el.tagName === 'SELECT' ||
|
el.tagName === 'INPUT' || el.tagName === 'SELECT' ||
|
||||||
@@ -310,4 +325,94 @@ func pngDimensions(png []byte) (int, int) {
|
|||||||
return w, h
|
return w, h
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ driver.DeviceDriver = (*Driver)(nil)
|
var (
|
||||||
|
_ driver.DeviceDriver = (*Driver)(nil)
|
||||||
|
_ driver.WebDriver = (*Driver)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// runCtx returns a chromedp-bound context that is also cancelled when the
|
||||||
|
// caller's ctx is cancelled. This is how step deadlines and Ctrl-C propagate
|
||||||
|
// into a CDP round-trip - chromedp.Run only honors the ctx it is given, and
|
||||||
|
// d.tabCtx alone has no link to the caller.
|
||||||
|
func (d *Driver) runCtx(ctx context.Context) (context.Context, context.CancelFunc) {
|
||||||
|
derived, cancel := context.WithCancel(d.tabCtx)
|
||||||
|
if ctx == nil || ctx.Done() == nil {
|
||||||
|
return derived, cancel
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
cancel()
|
||||||
|
case <-derived.Done():
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return derived, cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
// InstallBundle registers the source so it runs at every freshly-navigated
|
||||||
|
// document context, then immediately evaluates it against the current page so
|
||||||
|
// the very first tick has access to the registered globals.
|
||||||
|
func (d *Driver) InstallBundle(ctx context.Context, source []byte) error {
|
||||||
|
runCtx, cancel := d.runCtx(ctx)
|
||||||
|
defer cancel()
|
||||||
|
return chromedp.Run(runCtx,
|
||||||
|
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||||
|
if _, err := page.AddScriptToEvaluateOnNewDocument(string(source)).Do(ctx); err != nil {
|
||||||
|
return fmt.Errorf("addScriptToEvaluateOnNewDocument: %w", err)
|
||||||
|
}
|
||||||
|
_, exception, err := runtime.Evaluate(string(source)).Do(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("evaluate bundle: %w", err)
|
||||||
|
}
|
||||||
|
if exception != nil {
|
||||||
|
return fmt.Errorf("bundle threw: %s", exception.Text)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EvaluateExtractors invokes the bundle-installed extractor table and returns
|
||||||
|
// each extractor's JSON-encoded current value keyed by its registration index.
|
||||||
|
func (d *Driver) EvaluateExtractors(ctx context.Context) (map[int]json.RawMessage, error) {
|
||||||
|
const script = `JSON.stringify(window.__sanderlingExtractors__ ? window.__sanderlingExtractors__() : {})`
|
||||||
|
var encoded string
|
||||||
|
runCtx, cancel := d.runCtx(ctx)
|
||||||
|
defer cancel()
|
||||||
|
if err := chromedp.Run(runCtx, chromedp.Evaluate(script, &encoded)); err != nil {
|
||||||
|
return nil, fmt.Errorf("evaluate extractors: %w", err)
|
||||||
|
}
|
||||||
|
if encoded == "" || encoded == "{}" {
|
||||||
|
return map[int]json.RawMessage{}, nil
|
||||||
|
}
|
||||||
|
stringMap := map[string]json.RawMessage{}
|
||||||
|
if err := json.Unmarshal([]byte(encoded), &stringMap); err != nil {
|
||||||
|
return nil, fmt.Errorf("decode extractor map: %w", err)
|
||||||
|
}
|
||||||
|
result := make(map[int]json.RawMessage, len(stringMap))
|
||||||
|
for key, value := range stringMap {
|
||||||
|
index, err := strconv.Atoi(key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("non-integer extractor key %q", key)
|
||||||
|
}
|
||||||
|
result[index] = value
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NextActionFromV8 invokes the bundle-installed action generator and returns
|
||||||
|
// the resulting Action JSON. Returns an empty json.RawMessage when the
|
||||||
|
// generator declines to act this tick.
|
||||||
|
func (d *Driver) NextActionFromV8(ctx context.Context) (json.RawMessage, error) {
|
||||||
|
const script = `JSON.stringify(window.__sanderlingNextAction__ ? window.__sanderlingNextAction__() : null)`
|
||||||
|
var encoded string
|
||||||
|
runCtx, cancel := d.runCtx(ctx)
|
||||||
|
defer cancel()
|
||||||
|
if err := chromedp.Run(runCtx, chromedp.Evaluate(script, &encoded)); err != nil {
|
||||||
|
return nil, fmt.Errorf("evaluate next action: %w", err)
|
||||||
|
}
|
||||||
|
if encoded == "" || encoded == "null" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return json.RawMessage(encoded), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package chrome
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestRunCtx_CallerCancelPropagates confirms that cancelling the caller's
|
||||||
|
// context cancels the chromedp-bound context returned by runCtx. This is the
|
||||||
|
// channel by which step deadlines and Ctrl-C reach in-flight CDP calls.
|
||||||
|
func TestRunCtx_CallerCancelPropagates(t *testing.T) {
|
||||||
|
tabCtx, tabCancel := context.WithCancel(context.Background())
|
||||||
|
defer tabCancel()
|
||||||
|
d := &Driver{tabCtx: tabCtx}
|
||||||
|
|
||||||
|
callerCtx, callerCancel := context.WithCancel(context.Background())
|
||||||
|
derived, cancel := d.runCtx(callerCtx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
callerCancel()
|
||||||
|
select {
|
||||||
|
case <-derived.Done():
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("derived ctx did not cancel after caller cancellation")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRunCtx_TabCancelPropagates confirms the inverse: tearing down the tab
|
||||||
|
// also cancels any in-flight derived context.
|
||||||
|
func TestRunCtx_TabCancelPropagates(t *testing.T) {
|
||||||
|
tabCtx, tabCancel := context.WithCancel(context.Background())
|
||||||
|
d := &Driver{tabCtx: tabCtx}
|
||||||
|
|
||||||
|
derived, cancel := d.runCtx(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
tabCancel()
|
||||||
|
select {
|
||||||
|
case <-derived.Done():
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
t.Fatal("derived ctx did not cancel after tab cancellation")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package chrome
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// attrNamePattern matches HTML attribute names that are safe to drop into a
|
||||||
|
// CSS attribute selector without escaping. This avoids selectors like
|
||||||
|
// `foo]:has(*),body[x="..."]` that would escape the intended match.
|
||||||
|
var attrNamePattern = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9_-]*$`)
|
||||||
|
|
||||||
|
// TranslateStringSelector converts a legacy string selector ("id:foo",
|
||||||
|
// "descPrefix:bar") into a CSS selector or XPath expression usable from the
|
||||||
|
// chrome driver's TapSelector fallback path. The boolean return is true when
|
||||||
|
// the result is XPath rather than CSS. Unknown prefixes pass through to a CSS
|
||||||
|
// attribute match by the same name so a Maestro-side addition (e.g. a future
|
||||||
|
// "role:") works without a Sanderling release.
|
||||||
|
func TranslateStringSelector(selector string) (string, bool, error) {
|
||||||
|
if selector == "" {
|
||||||
|
return "", false, errors.New("empty selector")
|
||||||
|
}
|
||||||
|
colon := strings.IndexByte(selector, ':')
|
||||||
|
if colon <= 0 {
|
||||||
|
return "", false, errors.New("selector missing prefix (expected `kind:value`)")
|
||||||
|
}
|
||||||
|
kind := selector[:colon]
|
||||||
|
value := selector[colon+1:]
|
||||||
|
switch kind {
|
||||||
|
case "id", "resource-id":
|
||||||
|
return `[id="` + cssEscape(value) + `"]`, false, nil
|
||||||
|
case "class":
|
||||||
|
return `[class~="` + cssEscape(value) + `"]`, false, nil
|
||||||
|
case "tag":
|
||||||
|
return cssEscape(value), false, nil
|
||||||
|
case "text":
|
||||||
|
return `//*[normalize-space(text())=` + xpathStringLiteral(value) + `]`, true, nil
|
||||||
|
case "desc", "label", "content-desc", "accessibilityLabel", "accessibilityText", "ariaLabel", "aria-label":
|
||||||
|
return `[aria-label="` + cssEscape(value) + `"]`, false, nil
|
||||||
|
case "descPrefix":
|
||||||
|
return `[aria-label^="` + cssEscape(value) + `"]`, false, nil
|
||||||
|
case "testTag", "testID", "testid", "data-testid":
|
||||||
|
return `[data-testid="` + cssEscape(value) + `"]`, false, nil
|
||||||
|
case "placeholder", "placeholderValue", "hintText":
|
||||||
|
return `[placeholder="` + cssEscape(value) + `"]`, false, nil
|
||||||
|
default:
|
||||||
|
if !attrNamePattern.MatchString(kind) {
|
||||||
|
return "", false, fmt.Errorf("unsafe selector prefix %q", kind)
|
||||||
|
}
|
||||||
|
return `[` + kind + `="` + cssEscape(value) + `"]`, false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cssEscape escapes a value for use inside a CSS double-quoted string
|
||||||
|
// (`[attr="VALUE"]`). Per the CSSOM spec for serializing strings:
|
||||||
|
// - U+0000 becomes U+FFFD (REPLACEMENT CHARACTER)
|
||||||
|
// - control characters (U+0001-U+001F, U+007F) become \HEX escapes
|
||||||
|
// - " and \ are escaped with a leading backslash
|
||||||
|
// - everything else passes through, including non-ASCII
|
||||||
|
//
|
||||||
|
// Callers should not pass this output into identifier contexts (class names,
|
||||||
|
// tag names) — use an attribute selector form (`[class~="..."]`) instead.
|
||||||
|
func cssEscape(value string) string {
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.Grow(len(value))
|
||||||
|
for _, r := range value {
|
||||||
|
switch {
|
||||||
|
case r == 0:
|
||||||
|
builder.WriteRune(utf8.RuneError)
|
||||||
|
case (r >= 0x01 && r <= 0x1F) || r == 0x7F:
|
||||||
|
fmt.Fprintf(&builder, "\\%X ", r)
|
||||||
|
case r == '\\' || r == '"':
|
||||||
|
builder.WriteByte('\\')
|
||||||
|
builder.WriteRune(r)
|
||||||
|
default:
|
||||||
|
builder.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// xpathStringLiteral wraps the value in a valid XPath 1.0 string literal.
|
||||||
|
// XPath 1.0 has no escape syntax, so a value containing both ' and " must be
|
||||||
|
// composed via concat(). The output already includes the surrounding quotes
|
||||||
|
// (or concat() call), so callers don't quote it again.
|
||||||
|
func xpathStringLiteral(value string) string {
|
||||||
|
if !strings.ContainsRune(value, '"') {
|
||||||
|
return `"` + value + `"`
|
||||||
|
}
|
||||||
|
if !strings.ContainsRune(value, '\'') {
|
||||||
|
return `'` + value + `'`
|
||||||
|
}
|
||||||
|
parts := strings.Split(value, `"`)
|
||||||
|
var builder strings.Builder
|
||||||
|
builder.WriteString(`concat(`)
|
||||||
|
for index, part := range parts {
|
||||||
|
if index > 0 {
|
||||||
|
builder.WriteString(`, '"', `)
|
||||||
|
}
|
||||||
|
builder.WriteByte('"')
|
||||||
|
builder.WriteString(part)
|
||||||
|
builder.WriteByte('"')
|
||||||
|
}
|
||||||
|
builder.WriteByte(')')
|
||||||
|
return builder.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package chrome
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestTranslateStringSelector_KnownKeys(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
selector string
|
||||||
|
want string
|
||||||
|
isXPath bool
|
||||||
|
}{
|
||||||
|
{"id:email", `[id="email"]`, false},
|
||||||
|
{"resource-id:account-name", `[id="account-name"]`, false},
|
||||||
|
{"class:btn-primary", `[class~="btn-primary"]`, false},
|
||||||
|
{"tag:button", `button`, false},
|
||||||
|
{"text:Sign in", `//*[normalize-space(text())="Sign in"]`, true},
|
||||||
|
{`text:Say "hi"`, `//*[normalize-space(text())='Say "hi"']`, true},
|
||||||
|
{`text:it's`, `//*[normalize-space(text())="it's"]`, true},
|
||||||
|
{`text:it's "fine"`, `//*[normalize-space(text())=concat("it's ", '"', "fine", '"', "")]`, true},
|
||||||
|
{"desc:logout", `[aria-label="logout"]`, false},
|
||||||
|
{"label:logout", `[aria-label="logout"]`, false},
|
||||||
|
{"accessibilityLabel:logout", `[aria-label="logout"]`, false},
|
||||||
|
{"aria-label:Sign in", `[aria-label="Sign in"]`, false},
|
||||||
|
{"descPrefix:account:", `[aria-label^="account:"]`, false},
|
||||||
|
{"testTag:submit", `[data-testid="submit"]`, false},
|
||||||
|
{"testID:submit", `[data-testid="submit"]`, false},
|
||||||
|
{"placeholder:Email", `[placeholder="Email"]`, false},
|
||||||
|
}
|
||||||
|
for _, testCase := range cases {
|
||||||
|
got, isXPath, err := TranslateStringSelector(testCase.selector)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%q: unexpected error %v", testCase.selector, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if got != testCase.want || isXPath != testCase.isXPath {
|
||||||
|
t.Errorf("%q: got (%q, xpath=%v), want (%q, xpath=%v)",
|
||||||
|
testCase.selector, got, isXPath, testCase.want, testCase.isXPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTranslateStringSelector_UnknownPrefixPassesThrough(t *testing.T) {
|
||||||
|
got, _, err := TranslateStringSelector("role:button")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != `[role="button"]` {
|
||||||
|
t.Errorf("unknown prefix should map to attribute selector, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTranslateStringSelector_EscapesQuotes(t *testing.T) {
|
||||||
|
got, _, err := TranslateStringSelector(`label:say "hi"`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got != `[aria-label="say \"hi\""]` {
|
||||||
|
t.Errorf("expected escaped quotes, got %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTranslateStringSelector_RejectsMissingPrefix(t *testing.T) {
|
||||||
|
if _, _, err := TranslateStringSelector("foo"); err == nil {
|
||||||
|
t.Error("expected error for missing prefix")
|
||||||
|
}
|
||||||
|
if _, _, err := TranslateStringSelector(""); err == nil {
|
||||||
|
t.Error("expected error for empty selector")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTranslateStringSelector_RejectsUnsafePrefix(t *testing.T) {
|
||||||
|
cases := []string{
|
||||||
|
`foo]:has(*),body[x:value`,
|
||||||
|
`x y:value`,
|
||||||
|
`x"y:value`,
|
||||||
|
`*:value`,
|
||||||
|
`(:value`,
|
||||||
|
}
|
||||||
|
for _, selector := range cases {
|
||||||
|
if _, _, err := TranslateStringSelector(selector); err == nil {
|
||||||
|
t.Errorf("%q: expected error for unsafe prefix", selector)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCSSEscape_ControlCharactersAndNUL(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{"plain ascii", "hello", "hello"},
|
||||||
|
{"double quote", `say "hi"`, `say \"hi\"`},
|
||||||
|
{"backslash", `a\b`, `a\\b`},
|
||||||
|
{"newline", "a\nb", `a\A b`},
|
||||||
|
{"carriage return", "a\rb", `a\D b`},
|
||||||
|
{"form feed", "a\fb", `a\C b`},
|
||||||
|
{"NUL replaced", "a\x00b", "a�b"},
|
||||||
|
{"DEL", "a\x7Fb", `a\7F b`},
|
||||||
|
{"non-ascii passes through", "café", "café"},
|
||||||
|
}
|
||||||
|
for _, testCase := range cases {
|
||||||
|
got := cssEscape(testCase.input)
|
||||||
|
if got != testCase.want {
|
||||||
|
t.Errorf("%s: cssEscape(%q) = %q, want %q",
|
||||||
|
testCase.name, testCase.input, got, testCase.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package driver
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -58,3 +59,27 @@ type Metrics struct {
|
|||||||
HeapBytes int64
|
HeapBytes int64
|
||||||
TotalMemoryBytes int64
|
TotalMemoryBytes int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WebDriver is the optional capability surface exposed by the chrome driver
|
||||||
|
// for the V8-native tick path. The runner type-asserts on this interface;
|
||||||
|
// mobile drivers stay binary-compatible by simply not implementing it.
|
||||||
|
//
|
||||||
|
// Element references never cross V8/host. V8 serializes targets as {x, y}
|
||||||
|
// (or bounds) into the returned WebAction JSON; the host dispatches via the
|
||||||
|
// normal DeviceDriver methods (Tap, InputText, etc.).
|
||||||
|
type WebDriver interface {
|
||||||
|
// InstallBundle injects the given JS source so it runs once per
|
||||||
|
// freshly-navigated document, plus immediately in the current page.
|
||||||
|
// The bundle is expected to register globals
|
||||||
|
// `__sanderlingExtractors__` and `__sanderlingNextAction__` on
|
||||||
|
// `window`.
|
||||||
|
InstallBundle(ctx context.Context, source []byte) error
|
||||||
|
// EvaluateExtractors invokes the extractor table installed by the
|
||||||
|
// bundle and returns each extractor's JSON-encoded current value
|
||||||
|
// keyed by its registration index.
|
||||||
|
EvaluateExtractors(ctx context.Context) (map[int]json.RawMessage, error)
|
||||||
|
// NextActionFromV8 invokes the action generator installed by the
|
||||||
|
// bundle and returns the resulting Action JSON for the host to
|
||||||
|
// dispatch. The shape mirrors verifier.Action's JSON form.
|
||||||
|
NextActionFromV8(ctx context.Context) (json.RawMessage, error)
|
||||||
|
}
|
||||||
+113
-17
@@ -74,30 +74,48 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
|||||||
var metrics *trace.Metrics
|
var metrics *trace.Metrics
|
||||||
var logs []verifier.LogEntry
|
var logs []verifier.LogEntry
|
||||||
|
|
||||||
g, _ := errgroup.WithContext(ctx)
|
// gctx is bound to the errgroup so a returned error (or outer
|
||||||
|
// cancellation) propagates to siblings - notably the V8 extractor
|
||||||
|
// goroutine, whose CDP round-trip can otherwise outrun the step
|
||||||
|
// budget on a hung tab.
|
||||||
|
g, gctx := errgroup.WithContext(ctx)
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
tree, hierarchyErr = fetchHierarchy(ctx, options.Driver)
|
tree, hierarchyErr = fetchHierarchy(gctx, options.Driver)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
si := stepIndex
|
si := stepIndex
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
metrics = captureMetrics(ctx, options, logger, si)
|
metrics = captureMetrics(gctx, options, logger, si)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
logSince := lastLogTime
|
logSince := lastLogTime
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
logs = collectLogs(ctx, options.Driver, logSince)
|
logs = collectLogs(gctx, options.Driver, logSince)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
var v8Overrides map[int]json.RawMessage
|
||||||
|
if web, ok := options.Driver.(driver.WebDriver); ok {
|
||||||
|
g.Go(func() error {
|
||||||
|
overrides, err := web.EvaluateExtractors(gctx)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warn("v8 extractor evaluation failed", "step", si, "err", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v8Overrides = overrides
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
if pendingPostScreenshot {
|
if pendingPostScreenshot {
|
||||||
postStep := pendingPostScreenshotStep
|
postStep := pendingPostScreenshotStep
|
||||||
g.Go(func() error {
|
g.Go(func() error {
|
||||||
captureScreenshot(ctx, options, logger, postStep, true)
|
captureScreenshot(gctx, options, logger, postStep, true)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
pendingPostScreenshot = false
|
pendingPostScreenshot = false
|
||||||
}
|
}
|
||||||
g.Wait()
|
// All goroutines write to local variables and return nil, so the Wait
|
||||||
|
// error is always nil; ignored intentionally.
|
||||||
|
_ = g.Wait()
|
||||||
|
|
||||||
if hierarchyErr != nil {
|
if hierarchyErr != nil {
|
||||||
if isWDADrop(hierarchyErr) {
|
if isWDADrop(hierarchyErr) {
|
||||||
@@ -120,6 +138,14 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return summary, fmt.Errorf("step %d push: %w", stepIndex, err)
|
return summary, fmt.Errorf("step %d push: %w", stepIndex, err)
|
||||||
}
|
}
|
||||||
|
skipped, overrideErr := options.Verifier.OverrideExtractorValues(v8Overrides)
|
||||||
|
if overrideErr != nil {
|
||||||
|
logger.Warn("v8 override apply failed", "step", stepIndex, "err", overrideErr)
|
||||||
|
}
|
||||||
|
if skipped > 0 {
|
||||||
|
logger.Warn("v8 override skipped out-of-range entries",
|
||||||
|
"step", stepIndex, "skipped", skipped, "have", len(v8Overrides))
|
||||||
|
}
|
||||||
|
|
||||||
screen := ""
|
screen := ""
|
||||||
if tree != nil && len(tree.Elements) > 0 {
|
if tree != nil && len(tree.Elements) > 0 {
|
||||||
@@ -134,7 +160,13 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nextAction, nextErr := options.Verifier.NextAction()
|
var nextAction verifier.Action
|
||||||
|
var nextErr error
|
||||||
|
if web, ok := options.Driver.(driver.WebDriver); ok {
|
||||||
|
nextAction, nextErr = nextActionFromV8(ctx, web)
|
||||||
|
} else {
|
||||||
|
nextAction, nextErr = options.Verifier.NextAction()
|
||||||
|
}
|
||||||
var traceAction *trace.Action
|
var traceAction *trace.Action
|
||||||
if nextErr == nil {
|
if nextErr == nil {
|
||||||
traceAction = traceActionFor(nextAction, tree)
|
traceAction = traceActionFor(nextAction, tree)
|
||||||
@@ -148,14 +180,14 @@ func Run(ctx context.Context, options Options) (Summary, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
step := trace.Step{
|
step := trace.Step{
|
||||||
Index: stepIndex,
|
Index: stepIndex,
|
||||||
Timestamp: stepStart,
|
Timestamp: stepStart,
|
||||||
Screen: screen,
|
Screen: screen,
|
||||||
Action: traceAction,
|
Action: traceAction,
|
||||||
Violations: violations,
|
Violations: violations,
|
||||||
Hierarchy: tree,
|
Hierarchy: tree,
|
||||||
Residuals: residuals,
|
Residuals: residuals,
|
||||||
Metrics: metrics,
|
Metrics: metrics,
|
||||||
}
|
}
|
||||||
if err := options.TraceWriter.WriteStep(step); err != nil {
|
if err := options.TraceWriter.WriteStep(step); err != nil {
|
||||||
return summary, fmt.Errorf("step %d trace: %w", stepIndex, err)
|
return summary, fmt.Errorf("step %d trace: %w", stepIndex, err)
|
||||||
@@ -303,10 +335,18 @@ func collectLogs(ctx context.Context, drv driver.DeviceDriver, since time.Time)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resolveCoordinates(action verifier.Action, tree *hierarchy.Tree) (int, int, bool) {
|
func resolveCoordinates(action verifier.Action, tree *hierarchy.Tree) (int, int, bool) {
|
||||||
if action.X > 0 && action.Y > 0 {
|
// When On is empty, X/Y are authoritative (web V8 path emits coordinates
|
||||||
return action.X, action.Y, true
|
// directly from getBoundingClientRect; the runtime nullifies unresolved
|
||||||
|
// actions upstream so a non-null InputText here always has real coords,
|
||||||
|
// even at (0,0)). When On is set, prefer the tree lookup so stale coords
|
||||||
|
// don't leak from earlier ticks.
|
||||||
|
if action.On == "" {
|
||||||
|
if action.X >= 0 && action.Y >= 0 {
|
||||||
|
return action.X, action.Y, true
|
||||||
|
}
|
||||||
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
if tree != nil && action.On != "" {
|
if tree != nil {
|
||||||
if element := tree.Find(action.On); element != nil {
|
if element := tree.Find(action.On); element != nil {
|
||||||
x, y := element.Bounds.Center()
|
x, y := element.Bounds.Center()
|
||||||
if x > 0 && y > 0 {
|
if x > 0 && y > 0 {
|
||||||
@@ -314,6 +354,9 @@ func resolveCoordinates(action verifier.Action, tree *hierarchy.Tree) (int, int,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if action.X > 0 && action.Y > 0 {
|
||||||
|
return action.X, action.Y, true
|
||||||
|
}
|
||||||
return 0, 0, false
|
return 0, 0, false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +440,59 @@ func captureMetrics(ctx context.Context, options Options, logger *slog.Logger, s
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nextActionFromV8 invokes the V8-side action generator and decodes the
|
||||||
|
// resulting JSON into a verifier.Action. ErrNoAction is returned when the
|
||||||
|
// generator declined to act this tick.
|
||||||
|
func nextActionFromV8(ctx context.Context, web driver.WebDriver) (verifier.Action, error) {
|
||||||
|
raw, err := web.NextActionFromV8(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return verifier.Action{}, fmt.Errorf("v8 next action: %w", err)
|
||||||
|
}
|
||||||
|
if len(raw) == 0 || string(raw) == "null" {
|
||||||
|
return verifier.Action{}, verifier.ErrNoAction
|
||||||
|
}
|
||||||
|
var decoded struct {
|
||||||
|
Kind string `json:"kind"`
|
||||||
|
X int `json:"x"`
|
||||||
|
Y int `json:"y"`
|
||||||
|
FromX int `json:"from_x"`
|
||||||
|
FromY int `json:"from_y"`
|
||||||
|
ToX int `json:"to_x"`
|
||||||
|
ToY int `json:"to_y"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
DurationMillis int `json:"duration_millis"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &decoded); err != nil {
|
||||||
|
return verifier.Action{}, fmt.Errorf("decode v8 action: %w", err)
|
||||||
|
}
|
||||||
|
switch decoded.Kind {
|
||||||
|
case "Tap":
|
||||||
|
return verifier.Action{Kind: verifier.ActionKindTap, X: decoded.X, Y: decoded.Y}, nil
|
||||||
|
case "InputText":
|
||||||
|
return verifier.Action{
|
||||||
|
Kind: verifier.ActionKindInputText,
|
||||||
|
X: decoded.X, Y: decoded.Y,
|
||||||
|
Text: decoded.Text,
|
||||||
|
}, nil
|
||||||
|
case "Swipe":
|
||||||
|
return verifier.Action{
|
||||||
|
Kind: verifier.ActionKindSwipe,
|
||||||
|
FromX: decoded.FromX,
|
||||||
|
FromY: decoded.FromY,
|
||||||
|
ToX: decoded.ToX,
|
||||||
|
ToY: decoded.ToY,
|
||||||
|
DurationMillis: decoded.DurationMillis,
|
||||||
|
}, nil
|
||||||
|
case "PressKey":
|
||||||
|
return verifier.Action{Kind: verifier.ActionKindPressKey, Key: decoded.Key}, nil
|
||||||
|
case "Wait":
|
||||||
|
return verifier.Action{Kind: verifier.ActionKindWait, DurationMillis: decoded.DurationMillis}, nil
|
||||||
|
default:
|
||||||
|
return verifier.Action{}, verifier.ErrNoAction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func captureScreenshot(ctx context.Context, options Options, logger *slog.Logger, stepIndex int, after bool) {
|
func captureScreenshot(ctx context.Context, options Options, logger *slog.Logger, stepIndex int, after bool) {
|
||||||
image, err := options.Driver.Screenshot(ctx)
|
image, err := options.Driver.Screenshot(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -263,6 +263,37 @@ func TestApplyAction_InputTextSurfacesFocusTapError(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyAction_V8InputTextTapsAtCoordinates(t *testing.T) {
|
||||||
|
driverMock := mockdriver.New()
|
||||||
|
action := verifier.Action{Kind: verifier.ActionKindInputText, X: 50, Y: 100, Text: "alice"}
|
||||||
|
|
||||||
|
if err := applyAction(context.Background(), driverMock, action, nil); err != nil {
|
||||||
|
t.Fatalf("apply action: %v", err)
|
||||||
|
}
|
||||||
|
actions := driverMock.Actions()
|
||||||
|
if !containsAction(actions, mockdriver.ActionTap, "") {
|
||||||
|
t.Errorf("expected focus Tap before InputText, got %v", actions)
|
||||||
|
}
|
||||||
|
if !containsAction(actions, mockdriver.ActionInputText, "") {
|
||||||
|
t.Errorf("expected InputText after focus Tap, got %v", actions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyAction_V8InputTextAtOriginStillTaps(t *testing.T) {
|
||||||
|
driverMock := mockdriver.New()
|
||||||
|
// V8 emits real (0,0) coordinates for an element at viewport top-left
|
||||||
|
// (post-#15 the runtime nullifies unresolved actions, so a non-null
|
||||||
|
// InputText with (0,0) is a deliberate edge tap, not a sentinel).
|
||||||
|
action := verifier.Action{Kind: verifier.ActionKindInputText, X: 0, Y: 0, Text: "alice"}
|
||||||
|
|
||||||
|
if err := applyAction(context.Background(), driverMock, action, nil); err != nil {
|
||||||
|
t.Fatalf("apply action: %v", err)
|
||||||
|
}
|
||||||
|
if !containsAction(driverMock.Actions(), mockdriver.ActionTap, "") {
|
||||||
|
t.Errorf("expected focus Tap at (0,0), got %v", driverMock.Actions())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunner_ParallelFetchCallsAllDriverMethods(t *testing.T) {
|
func TestRunner_ParallelFetchCallsAllDriverMethods(t *testing.T) {
|
||||||
state := newHarness(t)
|
state := newHarness(t)
|
||||||
state.mock.MetricsData = driver.Metrics{CPUPercent: 5.0, HeapBytes: 1024, TotalMemoryBytes: 4096}
|
state.mock.MetricsData = driver.Metrics{CPUPercent: 5.0, HeapBytes: 1024, TotalMemoryBytes: 4096}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ import (
|
|||||||
// a cleanup function. For web, ChromeDriver is used directly; for android/ios
|
// a cleanup function. For web, ChromeDriver is used directly; for android/ios
|
||||||
// the JVM sidecar is extracted, spawned, and dialed.
|
// the JVM sidecar is extracted, spawned, and dialed.
|
||||||
func buildDriver(ctx context.Context, options Options, stdout io.Writer) (driver.DeviceDriver, func(), error) {
|
func buildDriver(ctx context.Context, options Options, stdout io.Writer) (driver.DeviceDriver, func(), error) {
|
||||||
|
if err := Preflight(ctx, options.Platform); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
if options.Platform == "web" {
|
if options.Platform == "web" {
|
||||||
d := chrome.New()
|
d := chrome.New()
|
||||||
return d, func() { _ = d.Terminate(context.Background()) }, nil
|
return d, func() { _ = d.Terminate(context.Background()) }, nil
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package testrun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Preflight runs platform-specific host checks before sidecar/driver setup.
|
||||||
|
// On failure it returns a wrapped error pointing the user at the matching
|
||||||
|
// `sanderling doctor --platform=<p>` command. Web returns nil (no host
|
||||||
|
// prerequisites beyond a working chromium, which the driver will surface
|
||||||
|
// itself if missing).
|
||||||
|
func Preflight(ctx context.Context, platform string) error {
|
||||||
|
check := preflightCheck
|
||||||
|
return runPreflight(ctx, platform, check)
|
||||||
|
}
|
||||||
|
|
||||||
|
type preflightFunc func(name string) error
|
||||||
|
|
||||||
|
func preflightCheck(name string) error {
|
||||||
|
if _, err := exec.LookPath(name); err != nil {
|
||||||
|
return fmt.Errorf("%s not found on PATH: %w", name, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runPreflight(ctx context.Context, platform string, check preflightFunc) error {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch platform {
|
||||||
|
case "web":
|
||||||
|
return nil
|
||||||
|
case "android":
|
||||||
|
if err := check("adb"); err != nil {
|
||||||
|
return preflightFailure("android", err)
|
||||||
|
}
|
||||||
|
if err := check("java"); err != nil {
|
||||||
|
return preflightFailure("android", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
case "ios":
|
||||||
|
if err := check("xcrun"); err != nil {
|
||||||
|
return preflightFailure("ios", err)
|
||||||
|
}
|
||||||
|
if err := check("java"); err != nil {
|
||||||
|
return preflightFailure("ios", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("preflight: unknown platform %q", platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func preflightFailure(platform string, cause error) error {
|
||||||
|
return fmt.Errorf(
|
||||||
|
"preflight: %w\nrun `sanderling doctor --platform=%s` for full host-readiness checks",
|
||||||
|
cause, platform,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package testrun
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPreflight_WebSkips(t *testing.T) {
|
||||||
|
called := 0
|
||||||
|
check := func(name string) error {
|
||||||
|
called++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := runPreflight(context.Background(), "web", check); err != nil {
|
||||||
|
t.Fatalf("web preflight should be no-op, got %v", err)
|
||||||
|
}
|
||||||
|
if called != 0 {
|
||||||
|
t.Errorf("web preflight ran %d binary checks; expected 0", called)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreflight_AndroidNeedsAdbAndJava(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
missing string
|
||||||
|
wantInErr string
|
||||||
|
}{
|
||||||
|
{name: "missing adb", missing: "adb", wantInErr: "adb"},
|
||||||
|
{name: "missing java", missing: "java", wantInErr: "java"},
|
||||||
|
}
|
||||||
|
for _, testCase := range cases {
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
check := func(name string) error {
|
||||||
|
if name == testCase.missing {
|
||||||
|
return errors.New(name + " not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := runPreflight(context.Background(), "android", check)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), testCase.wantInErr) {
|
||||||
|
t.Fatalf("expected error mentioning %q, got %v", testCase.wantInErr, err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "sanderling doctor --platform=android") {
|
||||||
|
t.Errorf("error missing doctor hint: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreflight_iOSNeedsXcrunAndJava(t *testing.T) {
|
||||||
|
check := func(name string) error {
|
||||||
|
if name == "xcrun" {
|
||||||
|
return errors.New("xcrun not found")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := runPreflight(context.Background(), "ios", check)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "xcrun") {
|
||||||
|
t.Fatalf("expected xcrun error, got %v", err)
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "sanderling doctor --platform=ios") {
|
||||||
|
t.Errorf("error missing doctor hint: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreflight_AllOK(t *testing.T) {
|
||||||
|
check := func(name string) error { return nil }
|
||||||
|
for _, platform := range []string{"web", "android", "ios"} {
|
||||||
|
if err := runPreflight(context.Background(), platform, check); err != nil {
|
||||||
|
t.Errorf("%s: unexpected error %v", platform, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreflight_UnknownPlatform(t *testing.T) {
|
||||||
|
check := func(string) error { return nil }
|
||||||
|
if err := runPreflight(context.Background(), "fuchsia", check); err == nil {
|
||||||
|
t.Error("expected error for unknown platform")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
|
|
||||||
"github.com/priyanshujain/sanderling/internal/android"
|
"github.com/priyanshujain/sanderling/internal/android"
|
||||||
"github.com/priyanshujain/sanderling/internal/bundler"
|
"github.com/priyanshujain/sanderling/internal/bundler"
|
||||||
|
"github.com/priyanshujain/sanderling/internal/driver"
|
||||||
"github.com/priyanshujain/sanderling/internal/ios"
|
"github.com/priyanshujain/sanderling/internal/ios"
|
||||||
"github.com/priyanshujain/sanderling/internal/runner"
|
"github.com/priyanshujain/sanderling/internal/runner"
|
||||||
"github.com/priyanshujain/sanderling/internal/trace"
|
"github.com/priyanshujain/sanderling/internal/trace"
|
||||||
@@ -45,24 +46,44 @@ func Execute(ctx context.Context, options Options, stdout io.Writer) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
aliases := map[string]string{}
|
aliases := map[string]string{}
|
||||||
if specAPIPath := resolveSpecAPIPath(options.Spec); specAPIPath != "" {
|
specAPIPath := resolveSpecAPIPath(options.Spec)
|
||||||
|
if specAPIPath != "" {
|
||||||
aliases["@sanderling/spec"] = specAPIPath
|
aliases["@sanderling/spec"] = specAPIPath
|
||||||
base := filepath.Dir(specAPIPath)
|
base := filepath.Dir(specAPIPath)
|
||||||
aliases["@sanderling/spec/defaults/properties"] = filepath.Join(base, "defaults/properties.ts")
|
aliases["@sanderling/spec/defaults/properties"] = filepath.Join(base, "defaults/properties.ts")
|
||||||
}
|
}
|
||||||
|
defines := map[string]string{
|
||||||
|
"SANDERLING_TEST_PHONE": os.Getenv("SANDERLING_TEST_PHONE"),
|
||||||
|
"SANDERLING_TEST_OTP": os.Getenv("SANDERLING_TEST_OTP"),
|
||||||
|
}
|
||||||
bundle, err := bundler.Bundle(bundler.Options{
|
bundle, err := bundler.Bundle(bundler.Options{
|
||||||
EntryFile: options.Spec,
|
EntryFile: options.Spec,
|
||||||
Defines: map[string]string{
|
Defines: defines,
|
||||||
"SANDERLING_TEST_PHONE": os.Getenv("SANDERLING_TEST_PHONE"),
|
Aliases: aliases,
|
||||||
"SANDERLING_TEST_OTP": os.Getenv("SANDERLING_TEST_OTP"),
|
|
||||||
},
|
|
||||||
Aliases: aliases,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("bundle spec: %w", err)
|
return fmt.Errorf("bundle spec: %w", err)
|
||||||
}
|
}
|
||||||
fmt.Fprintf(stdout, "bundled spec: %d bytes (sha256=%s)\n", len(bundle.JavaScript), bundle.SHA256[:12])
|
fmt.Fprintf(stdout, "bundled spec: %d bytes (sha256=%s)\n", len(bundle.JavaScript), bundle.SHA256[:12])
|
||||||
|
|
||||||
|
var webBundle bundler.Result
|
||||||
|
if options.Platform == "web" {
|
||||||
|
runtimePath := resolveWebRuntimePath(specAPIPath, options.Spec)
|
||||||
|
if runtimePath == "" {
|
||||||
|
return fmt.Errorf("web-runtime.ts not found near %s; checkout pkg/spec or set @sanderling/spec alias", options.Spec)
|
||||||
|
}
|
||||||
|
webBundle, err = bundler.BundleWeb(bundler.WebOptions{
|
||||||
|
EntryFile: options.Spec,
|
||||||
|
WebRuntimeFile: runtimePath,
|
||||||
|
Defines: defines,
|
||||||
|
Aliases: aliases,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("bundle web spec: %w", err)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(stdout, "bundled web spec: %d bytes (sha256=%s)\n", len(webBundle.JavaScript), webBundle.SHA256[:12])
|
||||||
|
}
|
||||||
|
|
||||||
activeDriver, cleanup, err := buildDriver(ctx, options, stdout)
|
activeDriver, cleanup, err := buildDriver(ctx, options, stdout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -73,6 +94,12 @@ func Execute(ctx context.Context, options Options, stdout io.Writer) error {
|
|||||||
return fmt.Errorf("launch app: %w", err)
|
return fmt.Errorf("launch app: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if web, ok := activeDriver.(driver.WebDriver); ok && len(webBundle.JavaScript) > 0 {
|
||||||
|
if err := web.InstallBundle(ctx, webBundle.JavaScript); err != nil {
|
||||||
|
return fmt.Errorf("install web bundle: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
seed := options.Seed
|
seed := options.Seed
|
||||||
if seed == 0 {
|
if seed == 0 {
|
||||||
seed = time.Now().UnixNano()
|
seed = time.Now().UnixNano()
|
||||||
@@ -142,6 +169,34 @@ func Execute(ctx context.Context, options Options, stdout io.Writer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveWebRuntimePath returns the path to pkg/spec/src/web-runtime.ts.
|
||||||
|
// Tries the spec-API checkout first (so monorepo development works without
|
||||||
|
// publishing the package), then falls back to a sibling of the resolved
|
||||||
|
// @sanderling/spec entry, and finally to a node_modules path.
|
||||||
|
func resolveWebRuntimePath(specAPIPath, userSpecPath string) string {
|
||||||
|
if specAPIPath != "" {
|
||||||
|
candidate := filepath.Join(filepath.Dir(specAPIPath), "web-runtime.ts")
|
||||||
|
if _, err := os.Stat(candidate); err == nil {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if absoluteSpec, err := filepath.Abs(userSpecPath); err == nil {
|
||||||
|
directory := filepath.Dir(absoluteSpec)
|
||||||
|
for {
|
||||||
|
candidate := filepath.Join(directory, "node_modules", "@sanderling", "spec", "src", "web-runtime.ts")
|
||||||
|
if _, err := os.Stat(candidate); err == nil {
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
parent := filepath.Dir(directory)
|
||||||
|
if parent == directory {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
directory = parent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// resolveSpecAPIPath returns the path to pkg/spec/src/index.ts inside
|
// resolveSpecAPIPath returns the path to pkg/spec/src/index.ts inside
|
||||||
// a sanderling source checkout, searched upward from the spec file and the cwd.
|
// a sanderling source checkout, searched upward from the spec file and the cwd.
|
||||||
// Returns "" when not found, in which case esbuild resolves @sanderling/spec via
|
// Returns "" when not found, in which case esbuild resolves @sanderling/spec via
|
||||||
|
|||||||
@@ -13,13 +13,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Step struct {
|
type Step struct {
|
||||||
Index int `json:"step"`
|
Index int `json:"step"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
Screen string `json:"screen,omitempty"`
|
Screen string `json:"screen,omitempty"`
|
||||||
Snapshots map[string]json.RawMessage `json:"snapshots,omitempty"`
|
Snapshots map[string]json.RawMessage `json:"snapshots,omitempty"`
|
||||||
Action *Action `json:"action,omitempty"`
|
Action *Action `json:"action,omitempty"`
|
||||||
Exceptions []Exception `json:"exceptions,omitempty"`
|
Exceptions []Exception `json:"exceptions,omitempty"`
|
||||||
Violations []string `json:"violations,omitempty"`
|
Violations []string `json:"violations,omitempty"`
|
||||||
Hierarchy *hierarchy.Tree `json:"hierarchy,omitempty"`
|
Hierarchy *hierarchy.Tree `json:"hierarchy,omitempty"`
|
||||||
Residuals map[string]json.RawMessage `json:"residuals,omitempty"`
|
Residuals map[string]json.RawMessage `json:"residuals,omitempty"`
|
||||||
Metrics *Metrics `json:"metrics,omitempty"`
|
Metrics *Metrics `json:"metrics,omitempty"`
|
||||||
|
|||||||
@@ -532,3 +532,106 @@ globalThis.properties = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestOverrideExtractorValues_PreservesPrevious(t *testing.T) {
|
||||||
|
verifier := newVerifier(t)
|
||||||
|
mustLoad(t, verifier, helloSpec)
|
||||||
|
|
||||||
|
if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{"ledger.balance": json.RawMessage(`100`)}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := verifier.OverrideExtractorValues(map[int]json.RawMessage{1: json.RawMessage(`777`)}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
balance := verifier.runtime.GlobalObject().Get("balance").ToObject(verifier.runtime)
|
||||||
|
if balance.Get("current").ToInteger() != 777 {
|
||||||
|
t.Errorf("override didn't take: current=%v", balance.Get("current"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next push: previous mirrors the *override*, not the snapshot value.
|
||||||
|
if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{"ledger.balance": json.RawMessage(`200`)}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
balance = verifier.runtime.GlobalObject().Get("balance").ToObject(verifier.runtime)
|
||||||
|
if balance.Get("previous").ToInteger() != 777 {
|
||||||
|
t.Errorf("previous should reflect override, got %v", balance.Get("previous"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverrideExtractorValues_NilIsNoop(t *testing.T) {
|
||||||
|
verifier := newVerifier(t)
|
||||||
|
mustLoad(t, verifier, helloSpec)
|
||||||
|
if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{"ledger.balance": json.RawMessage(`42`)}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := verifier.OverrideExtractorValues(nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := verifier.OverrideExtractorValues(map[int]json.RawMessage{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
balance := verifier.runtime.GlobalObject().Get("balance").ToObject(verifier.runtime)
|
||||||
|
if balance.Get("current").ToInteger() != 42 {
|
||||||
|
t.Errorf("expected snapshot-driven current to remain 42, got %v", balance.Get("current"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOverrideExtractorValues_UnknownIndexSkipped(t *testing.T) {
|
||||||
|
verifier := newVerifier(t)
|
||||||
|
mustLoad(t, verifier, helloSpec)
|
||||||
|
if err := verifier.PushSnapshot(SnapshotInput{Snapshots: Snapshots{"ledger.balance": json.RawMessage(`42`)}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
skipped, err := verifier.OverrideExtractorValues(map[int]json.RawMessage{
|
||||||
|
1: json.RawMessage(`777`),
|
||||||
|
99: json.RawMessage(`1`),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if skipped != 1 {
|
||||||
|
t.Errorf("expected 1 skipped entry, got %d", skipped)
|
||||||
|
}
|
||||||
|
balance := verifier.runtime.GlobalObject().Get("balance").ToObject(verifier.runtime)
|
||||||
|
if balance.Get("current").ToInteger() != 777 {
|
||||||
|
t.Errorf("valid override should still apply alongside skipped one, got current=%v", balance.Get("current"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const objectExtractorSpec = `
|
||||||
|
const card = __sanderling__.extract(state => ({attrs: {testTag: "default"}, balance: 0}));
|
||||||
|
globalThis.card = card;
|
||||||
|
|
||||||
|
globalThis.properties = {
|
||||||
|
hasTestTag: __sanderling__.always(() => typeof card.current.attrs.testTag === "string"),
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.actions = __sanderling__.actions(() => []);
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestOverrideExtractorValues_PropagatesNestedObjectFields(t *testing.T) {
|
||||||
|
verifier := newVerifier(t)
|
||||||
|
mustLoad(t, verifier, objectExtractorSpec)
|
||||||
|
|
||||||
|
if err := verifier.PushSnapshot(SnapshotInput{}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
override := json.RawMessage(`{"attrs": {"testTag": "account-card"}, "balance": 12345}`)
|
||||||
|
skipped, err := verifier.OverrideExtractorValues(map[int]json.RawMessage{0: override})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if skipped != 0 {
|
||||||
|
t.Errorf("unexpected skipped count: %d", skipped)
|
||||||
|
}
|
||||||
|
|
||||||
|
card := verifier.runtime.GlobalObject().Get("card").ToObject(verifier.runtime)
|
||||||
|
current := card.Get("current").ToObject(verifier.runtime)
|
||||||
|
attrs := current.Get("attrs").ToObject(verifier.runtime)
|
||||||
|
if got := attrs.Get("testTag").String(); got != "account-card" {
|
||||||
|
t.Errorf("nested override missing: card.current.attrs.testTag = %q, want %q", got, "account-card")
|
||||||
|
}
|
||||||
|
if got := current.Get("balance").ToInteger(); got != 12345 {
|
||||||
|
t.Errorf("scalar field missing: card.current.balance = %d, want 12345", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package verifier
|
package verifier
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
@@ -238,6 +239,37 @@ func (v *Verifier) PushSnapshot(input SnapshotInput) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OverrideExtractorValues replaces each extractor's `current` slot with a
|
||||||
|
// caller-supplied value, keyed by registration index. Used by the web tick
|
||||||
|
// path so extractor bodies that ran in V8 (against the real DOM) drive the
|
||||||
|
// goja-side LTL predicates without re-running the getter against an empty
|
||||||
|
// state.ax shim. Passing a nil/empty map is a no-op so the mobile path can
|
||||||
|
// call this unconditionally. The override must run *after* PushSnapshot
|
||||||
|
// (which advanced `previous`) and *before* EvaluateProperties.
|
||||||
|
//
|
||||||
|
// Out-of-range indices are tolerated (skipped) rather than fatal: V8 and goja
|
||||||
|
// register extractors from the same spec bundle so counts should always
|
||||||
|
// match, but a stale or partial override map should not block valid overrides
|
||||||
|
// from applying. The number of skipped entries is reported so the caller can
|
||||||
|
// surface a mismatch.
|
||||||
|
func (v *Verifier) OverrideExtractorValues(overrides map[int]json.RawMessage) (skipped int, err error) {
|
||||||
|
if len(overrides) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
for index, raw := range overrides {
|
||||||
|
if index < 0 || index >= len(v.extractors) {
|
||||||
|
skipped++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, conversionErr := jsonToJSValue(v.runtime, raw)
|
||||||
|
if conversionErr != nil {
|
||||||
|
return skipped, fmt.Errorf("extractor override %d: %w", index, conversionErr)
|
||||||
|
}
|
||||||
|
_ = v.extractors[index].handle.Set("current", value)
|
||||||
|
}
|
||||||
|
return skipped, nil
|
||||||
|
}
|
||||||
|
|
||||||
// SnapshotInput bundles everything a step feeds into the verifier. Fields
|
// SnapshotInput bundles everything a step feeds into the verifier. Fields
|
||||||
// other than Snapshots are optional; callers that only have snapshots can
|
// other than Snapshots are optional; callers that only have snapshots can
|
||||||
// populate Snapshots alone and leave the rest zero.
|
// populate Snapshots alone and leave the rest zero.
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type {
|
|||||||
TapAction,
|
TapAction,
|
||||||
SanderlingRuntime,
|
SanderlingRuntime,
|
||||||
WaitAction,
|
WaitAction,
|
||||||
|
WebState,
|
||||||
WeightedEntry,
|
WeightedEntry,
|
||||||
} from "./types.ts";
|
} from "./types.ts";
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/// <reference lib="dom" />
|
||||||
|
|
||||||
export type Snapshots = Record<string, unknown>;
|
export type Snapshots = Record<string, unknown>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -10,20 +12,28 @@ export type Snapshots = Record<string, unknown>;
|
|||||||
*/
|
*/
|
||||||
export interface KnownAttrSelectors {
|
export interface KnownAttrSelectors {
|
||||||
testTag?: string;
|
testTag?: string;
|
||||||
|
testID?: string;
|
||||||
identifier?: string;
|
identifier?: string;
|
||||||
accessibilityIdentifier?: string;
|
accessibilityIdentifier?: string;
|
||||||
"resource-id"?: string;
|
"resource-id"?: string;
|
||||||
|
id?: string;
|
||||||
|
|
||||||
"content-desc"?: string;
|
"content-desc"?: string;
|
||||||
|
contentDescription?: string;
|
||||||
accessibilityText?: string;
|
accessibilityText?: string;
|
||||||
accessibilityLabel?: string;
|
accessibilityLabel?: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
"aria-label"?: string;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
|
||||||
text?: string;
|
text?: string;
|
||||||
|
tag?: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
|
className?: string;
|
||||||
elementType?: string;
|
elementType?: string;
|
||||||
package?: string;
|
package?: string;
|
||||||
placeholderValue?: string;
|
placeholderValue?: string;
|
||||||
|
placeholder?: string;
|
||||||
hintText?: string;
|
hintText?: string;
|
||||||
|
|
||||||
clickable?: boolean;
|
clickable?: boolean;
|
||||||
@@ -99,6 +109,17 @@ export interface State {
|
|||||||
exceptions: readonly ExceptionRecord[];
|
exceptions: readonly ExceptionRecord[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State as observed inside a web (V8/browser) extractor. Adds the live
|
||||||
|
* `document` and `window` handles. `state.document` is V8-only — goja-side
|
||||||
|
* predicates do not see it; if you need DOM data in a predicate, surface it
|
||||||
|
* via an `extract()`.
|
||||||
|
*/
|
||||||
|
export interface WebState extends State {
|
||||||
|
readonly document: Document;
|
||||||
|
readonly window: Window;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Extracted<T> {
|
export interface Extracted<T> {
|
||||||
readonly current: T;
|
readonly current: T;
|
||||||
readonly previous: T | undefined;
|
readonly previous: T | undefined;
|
||||||
|
|||||||
@@ -0,0 +1,582 @@
|
|||||||
|
/// <reference lib="dom" />
|
||||||
|
|
||||||
|
// V8-side runtime for `sanderling test --platform web`.
|
||||||
|
//
|
||||||
|
// The user spec is bundled with this file as the first import so that
|
||||||
|
// globalThis.__sanderling__ is installed before the spec evaluates. The host
|
||||||
|
// invokes window.__sanderlingExtractors__() and window.__sanderlingNextAction__()
|
||||||
|
// over CDP each tick. LTL predicates are intentionally stubbed: properties
|
||||||
|
// run host-side in goja, which loads its own bundle of the same spec.
|
||||||
|
//
|
||||||
|
// Element references never cross V8/host. Action targets that reference an
|
||||||
|
// AccessibilityElement collapse to `{x, y}` from getBoundingClientRect()
|
||||||
|
// before serialization.
|
||||||
|
|
||||||
|
interface Handle {
|
||||||
|
current: unknown;
|
||||||
|
previous: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ExtractorEntry {
|
||||||
|
getter: (state: unknown) => unknown;
|
||||||
|
handle: Handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ActionGeneratorHandle {
|
||||||
|
__sanderlingActionGenerator: true;
|
||||||
|
__sanderlingKind: string;
|
||||||
|
generate?: () => unknown;
|
||||||
|
entries?: readonly [number, ActionGeneratorHandle][];
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractors: ExtractorEntry[] = [];
|
||||||
|
let actionsRoot: ActionGeneratorHandle | null = null;
|
||||||
|
|
||||||
|
function noopFormula(): unknown {
|
||||||
|
const formula: Record<string, unknown> = { __sanderlingFormula: true };
|
||||||
|
formula.implies = () => formula;
|
||||||
|
formula.or = () => formula;
|
||||||
|
formula.and = () => formula;
|
||||||
|
formula.not = () => formula;
|
||||||
|
formula.within = () => formula;
|
||||||
|
return formula;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KNOWN_KEY_TO_CSS: Record<string, (value: string) => string> = {
|
||||||
|
id: (v) => `[id="${cssEscape(v)}"]`,
|
||||||
|
"resource-id": (v) => `[id="${cssEscape(v)}"]`,
|
||||||
|
testTag: (v) => `[data-testid="${cssEscape(v)}"]`,
|
||||||
|
testID: (v) => `[data-testid="${cssEscape(v)}"]`,
|
||||||
|
"data-testid": (v) => `[data-testid="${cssEscape(v)}"]`,
|
||||||
|
className: (v) => `[class~="${cssEscape(v)}"]`,
|
||||||
|
class: (v) => `[class~="${cssEscape(v)}"]`,
|
||||||
|
tag: tagSelector,
|
||||||
|
"aria-label": (v) => `[aria-label="${cssEscape(v)}"]`,
|
||||||
|
ariaLabel: (v) => `[aria-label="${cssEscape(v)}"]`,
|
||||||
|
accessibilityLabel: (v) => `[aria-label="${cssEscape(v)}"]`,
|
||||||
|
contentDescription: (v) => `[aria-label="${cssEscape(v)}"]`,
|
||||||
|
"content-desc": (v) => `[aria-label="${cssEscape(v)}"]`,
|
||||||
|
label: (v) => `[aria-label="${cssEscape(v)}"]`,
|
||||||
|
placeholder: (v) => `[placeholder="${cssEscape(v)}"]`,
|
||||||
|
placeholderValue: (v) => `[placeholder="${cssEscape(v)}"]`,
|
||||||
|
hintText: (v) => `[placeholder="${cssEscape(v)}"]`,
|
||||||
|
};
|
||||||
|
|
||||||
|
// cssEscape delegates to the platform CSS.escape (per CSSOM spec). It produces
|
||||||
|
// output safe for both identifier and string contexts, since CSS string
|
||||||
|
// literals accept the same `\HEX ` and `\X` escape sequences as identifiers.
|
||||||
|
function cssEscape(value: string): string {
|
||||||
|
return CSS.escape(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TAG_NAME = /^[a-zA-Z][a-zA-Z0-9-]*$/;
|
||||||
|
|
||||||
|
// tagSelector accepts only valid HTML tag-name characters. Anything else (a
|
||||||
|
// pseudo-class like `*:hover`, a comma, whitespace) would inject CSS into the
|
||||||
|
// surrounding selector. Returning a never-matching selector rather than
|
||||||
|
// throwing keeps the spec running while making the typo visible in logs.
|
||||||
|
function tagSelector(value: string): string {
|
||||||
|
if (!TAG_NAME.test(value)) return ":not(*)";
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectorFromObject(selector: Record<string, string | boolean | undefined>): {
|
||||||
|
css?: string;
|
||||||
|
xpath?: string;
|
||||||
|
} {
|
||||||
|
const parts: string[] = [];
|
||||||
|
let textValue: string | undefined;
|
||||||
|
let descPrefix: string | undefined;
|
||||||
|
for (const key of Object.keys(selector)) {
|
||||||
|
const raw = selector[key];
|
||||||
|
if (raw === undefined) continue;
|
||||||
|
const value = typeof raw === "boolean" ? String(raw) : raw;
|
||||||
|
if (key === "text") {
|
||||||
|
textValue = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (key === "descPrefix") {
|
||||||
|
descPrefix = value;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const builder = KNOWN_KEY_TO_CSS[key];
|
||||||
|
if (builder) {
|
||||||
|
parts.push(builder(value));
|
||||||
|
} else {
|
||||||
|
parts.push(`[${key}="${cssEscape(value)}"]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (descPrefix !== undefined) {
|
||||||
|
parts.push(`[aria-label^="${cssEscape(descPrefix)}"]`);
|
||||||
|
}
|
||||||
|
if (textValue !== undefined && parts.length === 0) {
|
||||||
|
return {
|
||||||
|
xpath: `//*[normalize-space(text())=${xpathStringLiteral(textValue)}]`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { css: parts.join("") };
|
||||||
|
}
|
||||||
|
|
||||||
|
// xpathStringLiteral wraps the value in a valid XPath 1.0 string literal.
|
||||||
|
// XPath 1.0 has no escape syntax, so a value containing both ' and " must be
|
||||||
|
// composed via concat().
|
||||||
|
function xpathStringLiteral(value: string): string {
|
||||||
|
if (!value.includes('"')) return `"${value}"`;
|
||||||
|
if (!value.includes("'")) return `'${value}'`;
|
||||||
|
const parts = value.split('"');
|
||||||
|
return `concat(${parts.map((p) => `"${p}"`).join(`, '"', `)})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectorFromString(selector: string): { css?: string; xpath?: string } {
|
||||||
|
const colon = selector.indexOf(":");
|
||||||
|
if (colon <= 0) {
|
||||||
|
return { css: selector };
|
||||||
|
}
|
||||||
|
const kind = selector.slice(0, colon);
|
||||||
|
const value = selector.slice(colon + 1);
|
||||||
|
if (kind === "text") {
|
||||||
|
return { xpath: `//*[normalize-space(text())=${xpathStringLiteral(value)}]` };
|
||||||
|
}
|
||||||
|
if (kind === "descPrefix") {
|
||||||
|
return { css: `[aria-label^="${cssEscape(value)}"]` };
|
||||||
|
}
|
||||||
|
return selectorFromObject({ [kind]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryElement(
|
||||||
|
root: ParentNode,
|
||||||
|
selector: unknown,
|
||||||
|
): Element | null {
|
||||||
|
if (typeof selector === "string") {
|
||||||
|
const { css, xpath } = selectorFromString(selector);
|
||||||
|
if (css) return root.querySelector(css);
|
||||||
|
if (xpath) {
|
||||||
|
const result = document.evaluate(
|
||||||
|
xpath,
|
||||||
|
root as Node,
|
||||||
|
null,
|
||||||
|
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
return result.singleNodeValue as Element | null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (Array.isArray(selector)) {
|
||||||
|
let node: ParentNode | null = root;
|
||||||
|
for (const segment of selector) {
|
||||||
|
if (!node) return null;
|
||||||
|
const next = queryElement(node, segment);
|
||||||
|
if (!next) return null;
|
||||||
|
node = next;
|
||||||
|
}
|
||||||
|
return node as Element;
|
||||||
|
}
|
||||||
|
if (selector && typeof selector === "object") {
|
||||||
|
const { css, xpath } = selectorFromObject(selector as Record<string, string | boolean | undefined>);
|
||||||
|
if (css) return root.querySelector(css);
|
||||||
|
if (xpath) {
|
||||||
|
const result = document.evaluate(
|
||||||
|
xpath,
|
||||||
|
root as Node,
|
||||||
|
null,
|
||||||
|
XPathResult.FIRST_ORDERED_NODE_TYPE,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
return result.singleNodeValue as Element | null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryAllElements(root: ParentNode, selector: unknown): Element[] {
|
||||||
|
if (typeof selector === "string") {
|
||||||
|
const { css, xpath } = selectorFromString(selector);
|
||||||
|
if (css) return Array.from(root.querySelectorAll(css));
|
||||||
|
if (xpath) return evaluateXPathAll(xpath, root as Node);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (selector && typeof selector === "object" && !Array.isArray(selector)) {
|
||||||
|
const { css, xpath } = selectorFromObject(selector as Record<string, string | boolean | undefined>);
|
||||||
|
if (css) return Array.from(root.querySelectorAll(css));
|
||||||
|
if (xpath) return evaluateXPathAll(xpath, root as Node);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateXPathAll(xpath: string, root: Node): Element[] {
|
||||||
|
const result = document.evaluate(
|
||||||
|
xpath,
|
||||||
|
root,
|
||||||
|
null,
|
||||||
|
XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const out: Element[] = [];
|
||||||
|
for (let i = 0; i < result.snapshotLength; i++) {
|
||||||
|
const node = result.snapshotItem(i);
|
||||||
|
if (node) out.push(node as Element);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function elementHandle(element: Element): Record<string, unknown> {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const x = Math.round(rect.left + rect.width / 2);
|
||||||
|
const y = Math.round(rect.top + rect.height / 2);
|
||||||
|
const ariaLabel = element.getAttribute("aria-label") ?? "";
|
||||||
|
const text = (element.textContent ?? "").trim().slice(0, 200);
|
||||||
|
const datasetCopy: Record<string, string> = {};
|
||||||
|
const dataset = (element as HTMLElement).dataset ?? {};
|
||||||
|
for (const key of Object.keys(dataset)) {
|
||||||
|
const value = (dataset as Record<string, string | undefined>)[key];
|
||||||
|
if (value !== undefined) datasetCopy[key] = value;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: element.id,
|
||||||
|
text,
|
||||||
|
desc: ariaLabel,
|
||||||
|
class: (element as HTMLElement).className ?? "",
|
||||||
|
clickable: true,
|
||||||
|
enabled: !(element as HTMLButtonElement).disabled,
|
||||||
|
focused: document.activeElement === element,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
bounds: {
|
||||||
|
left: Math.round(rect.left),
|
||||||
|
top: Math.round(rect.top),
|
||||||
|
right: Math.round(rect.right),
|
||||||
|
bottom: Math.round(rect.bottom),
|
||||||
|
},
|
||||||
|
attrs: {
|
||||||
|
tag: element.tagName.toLowerCase(),
|
||||||
|
"aria-label": ariaLabel,
|
||||||
|
...datasetCopy,
|
||||||
|
},
|
||||||
|
dataset: datasetCopy,
|
||||||
|
find(selector: unknown): unknown {
|
||||||
|
const child = queryElement(element, selector);
|
||||||
|
return child ? elementHandle(child) : undefined;
|
||||||
|
},
|
||||||
|
findAll(selector: unknown): unknown[] {
|
||||||
|
return queryAllElements(element, selector).map(elementHandle);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildAx(): unknown {
|
||||||
|
return {
|
||||||
|
find(selector: unknown): unknown {
|
||||||
|
const element = queryElement(document, selector);
|
||||||
|
return element ? elementHandle(element) : undefined;
|
||||||
|
},
|
||||||
|
findAll(selector: unknown): unknown[] {
|
||||||
|
return queryAllElements(document, selector).map(elementHandle);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildState(): unknown {
|
||||||
|
return {
|
||||||
|
snapshots: {},
|
||||||
|
ax: buildAx(),
|
||||||
|
document,
|
||||||
|
window,
|
||||||
|
lastAction: null,
|
||||||
|
time: 0,
|
||||||
|
logs: [],
|
||||||
|
exceptions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const runtime = {
|
||||||
|
extract<T>(getter: (state: unknown) => T): Handle {
|
||||||
|
const handle: Handle = { current: undefined, previous: undefined };
|
||||||
|
extractors.push({ getter: getter as (s: unknown) => unknown, handle });
|
||||||
|
return handle;
|
||||||
|
},
|
||||||
|
always: noopFormula,
|
||||||
|
now: noopFormula,
|
||||||
|
next: noopFormula,
|
||||||
|
eventually: noopFormula,
|
||||||
|
actions(generator: () => unknown): ActionGeneratorHandle {
|
||||||
|
const handle: ActionGeneratorHandle = {
|
||||||
|
__sanderlingActionGenerator: true,
|
||||||
|
__sanderlingKind: "actions",
|
||||||
|
generate: generator,
|
||||||
|
};
|
||||||
|
if (!actionsRoot) actionsRoot = handle;
|
||||||
|
return handle;
|
||||||
|
},
|
||||||
|
weighted(...entries: [number, ActionGeneratorHandle][]): ActionGeneratorHandle {
|
||||||
|
const handle: ActionGeneratorHandle = {
|
||||||
|
__sanderlingActionGenerator: true,
|
||||||
|
__sanderlingKind: "weighted",
|
||||||
|
entries,
|
||||||
|
};
|
||||||
|
actionsRoot = handle;
|
||||||
|
return handle;
|
||||||
|
},
|
||||||
|
from<T>(items: readonly T[]): { generate: () => T | undefined } {
|
||||||
|
return {
|
||||||
|
generate(): T | undefined {
|
||||||
|
if (items.length === 0) return undefined;
|
||||||
|
return items[Math.floor(Math.random() * items.length)];
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
tap(p: { on: unknown }): unknown {
|
||||||
|
return { kind: "Tap", on: p.on };
|
||||||
|
},
|
||||||
|
inputText(p: { into: unknown; text: string }): unknown {
|
||||||
|
return { kind: "InputText", into: p.into, text: p.text };
|
||||||
|
},
|
||||||
|
swipe(_p: { from: unknown; to: unknown; durationMillis?: number }): unknown {
|
||||||
|
// Why: web has no swipe gesture; the factory returns null so Swipe() calls in specs no-op.
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
pressKey(p: { key: string }): unknown {
|
||||||
|
if (!WEB_PRESS_KEYS.includes(p.key)) {
|
||||||
|
throw new Error(
|
||||||
|
`pressKey: unsupported key ${JSON.stringify(p.key)} (allowed: ${WEB_PRESS_KEYS.join(", ")})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return { kind: "PressKey", key: p.key };
|
||||||
|
},
|
||||||
|
wait(p: { durationMillis: number }): unknown {
|
||||||
|
return { kind: "Wait", durationMillis: p.durationMillis };
|
||||||
|
},
|
||||||
|
taps: { __sanderlingActionGenerator: true, __sanderlingKind: "taps" } as ActionGeneratorHandle,
|
||||||
|
swipes: { __sanderlingActionGenerator: true, __sanderlingKind: "swipes" } as ActionGeneratorHandle,
|
||||||
|
waitOnce: { __sanderlingActionGenerator: true, __sanderlingKind: "waitOnce" } as ActionGeneratorHandle,
|
||||||
|
pressKeys: { __sanderlingActionGenerator: true, __sanderlingKind: "pressKey" } as ActionGeneratorHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lock the runtime globals so a misbehaving (or malicious) page script can't
|
||||||
|
// shadow or replace them between AddScriptToEvaluateOnNewDocument running and
|
||||||
|
// the host invoking the extractor/next-action callbacks.
|
||||||
|
defineLockedGlobal("__sanderling__", runtime);
|
||||||
|
|
||||||
|
function defineLockedGlobal(name: string, value: unknown): void {
|
||||||
|
Object.defineProperty(globalThis, name, {
|
||||||
|
value,
|
||||||
|
writable: false,
|
||||||
|
configurable: false,
|
||||||
|
enumerable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function evaluateExtractors(): Record<number, unknown> {
|
||||||
|
const state = buildState();
|
||||||
|
const result: Record<number, unknown> = {};
|
||||||
|
for (let i = 0; i < extractors.length; i++) {
|
||||||
|
const entry = extractors[i];
|
||||||
|
if (!entry) continue;
|
||||||
|
entry.handle.previous = entry.handle.current;
|
||||||
|
let value: unknown;
|
||||||
|
try {
|
||||||
|
value = entry.getter(state);
|
||||||
|
} catch {
|
||||||
|
value = undefined;
|
||||||
|
}
|
||||||
|
entry.handle.current = value;
|
||||||
|
result[i] = sanitize(value);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// SANITIZE_MAX_DEPTH bounds how far sanitize will recurse. State exposes
|
||||||
|
// `document` and `window`, both of which contain cycles; without a depth or
|
||||||
|
// seen-set guard a user extractor returning either crashes the runtime via
|
||||||
|
// stack overflow.
|
||||||
|
const SANITIZE_MAX_DEPTH = 32;
|
||||||
|
|
||||||
|
function sanitize(value: unknown): unknown {
|
||||||
|
return sanitizeAt(value, 0, new WeakSet());
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeAt(value: unknown, depth: number, seen: WeakSet<object>): unknown {
|
||||||
|
if (value === null || value === undefined) return value;
|
||||||
|
if (typeof value === "function") return undefined;
|
||||||
|
if (typeof value !== "object") return value;
|
||||||
|
if (depth >= SANITIZE_MAX_DEPTH) return null;
|
||||||
|
if (seen.has(value as object)) return null;
|
||||||
|
seen.add(value as object);
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => sanitizeAt(item, depth + 1, seen));
|
||||||
|
}
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const key of Object.keys(value as Record<string, unknown>)) {
|
||||||
|
const sub = (value as Record<string, unknown>)[key];
|
||||||
|
if (typeof sub === "function") continue;
|
||||||
|
out[key] = sanitizeAt(sub, depth + 1, seen);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickWeighted(handle: ActionGeneratorHandle): ActionGeneratorHandle | null {
|
||||||
|
const entries = handle.entries ?? [];
|
||||||
|
if (entries.length === 0) return null;
|
||||||
|
let total = 0;
|
||||||
|
for (const [weight] of entries) total += Math.max(0, weight);
|
||||||
|
if (total <= 0) return null;
|
||||||
|
let pick = Math.random() * total;
|
||||||
|
for (const [weight, generator] of entries) {
|
||||||
|
pick -= Math.max(0, weight);
|
||||||
|
if (pick <= 0) return generator;
|
||||||
|
}
|
||||||
|
return entries[entries.length - 1]?.[1] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveGenerator(handle: ActionGeneratorHandle): unknown {
|
||||||
|
switch (handle.__sanderlingKind) {
|
||||||
|
case "actions": {
|
||||||
|
const generated = handle.generate?.();
|
||||||
|
return pickFromArray(generated);
|
||||||
|
}
|
||||||
|
case "weighted": {
|
||||||
|
const inner = pickWeighted(handle);
|
||||||
|
if (!inner) return null;
|
||||||
|
return resolveGenerator(inner);
|
||||||
|
}
|
||||||
|
case "taps":
|
||||||
|
return randomTap();
|
||||||
|
case "swipes":
|
||||||
|
return randomSwipe();
|
||||||
|
case "waitOnce":
|
||||||
|
return { kind: "Wait", durationMillis: 500 };
|
||||||
|
case "pressKey":
|
||||||
|
return randomPressKey();
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-tick cache so the 16-attempt retry loop in __sanderlingNextAction__
|
||||||
|
// doesn't re-walk the DOM and re-flush layout on every iteration.
|
||||||
|
let randomTapCandidates: HTMLElement[] | null = null;
|
||||||
|
|
||||||
|
function randomTap(): unknown {
|
||||||
|
if (!randomTapCandidates) {
|
||||||
|
randomTapCandidates = Array.from(
|
||||||
|
document.querySelectorAll<HTMLElement>(
|
||||||
|
'a, button, input, select, textarea, [role="button"], [onclick]',
|
||||||
|
),
|
||||||
|
).filter((element) => {
|
||||||
|
if ((element as HTMLButtonElement).disabled) return false;
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return rect.width > 0 && rect.height > 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const candidates = randomTapCandidates;
|
||||||
|
if (candidates.length === 0) return null;
|
||||||
|
const picked = candidates[Math.floor(Math.random() * candidates.length)];
|
||||||
|
if (!picked) return null;
|
||||||
|
const rect = picked.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
kind: "Tap",
|
||||||
|
on: {
|
||||||
|
x: Math.round(rect.left + rect.width / 2),
|
||||||
|
y: Math.round(rect.top + rect.height / 2),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomSwipe(): unknown {
|
||||||
|
// Why: web has no swipe gesture; pointer drags into empty divs are noise.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const WEB_PRESS_KEYS = ["enter", "tab", "escape", "up", "down", "left", "right"];
|
||||||
|
|
||||||
|
function randomPressKey(): unknown {
|
||||||
|
// Why: only emit keys with meaningful browser semantics; "back"/"home" don't navigate.
|
||||||
|
const key = WEB_PRESS_KEYS[Math.floor(Math.random() * WEB_PRESS_KEYS.length)];
|
||||||
|
return { kind: "PressKey", key };
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickFromArray(value: unknown): unknown {
|
||||||
|
if (!value) return null;
|
||||||
|
if (!Array.isArray(value)) return value;
|
||||||
|
if (value.length === 0) return null;
|
||||||
|
return value[Math.floor(Math.random() * value.length)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeAction(action: unknown): unknown {
|
||||||
|
if (!action || typeof action !== "object") return null;
|
||||||
|
const obj = action as Record<string, unknown>;
|
||||||
|
switch (obj.kind) {
|
||||||
|
case "Tap": {
|
||||||
|
const point = pointOf(obj.on);
|
||||||
|
if (!point) {
|
||||||
|
console.warn("[sanderling] Tap target did not resolve to coordinates");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return { kind: "Tap", x: point.x, y: point.y };
|
||||||
|
}
|
||||||
|
case "InputText": {
|
||||||
|
const point = pointOf(obj.into);
|
||||||
|
if (!point) {
|
||||||
|
console.warn("[sanderling] InputText target did not resolve to coordinates");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: "InputText",
|
||||||
|
x: point.x,
|
||||||
|
y: point.y,
|
||||||
|
text: obj.text ?? "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "Swipe": {
|
||||||
|
const from = pointOf(obj.from);
|
||||||
|
const to = pointOf(obj.to);
|
||||||
|
if (!from || !to) {
|
||||||
|
console.warn("[sanderling] Swipe endpoints did not resolve to coordinates");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: "Swipe",
|
||||||
|
from_x: from.x,
|
||||||
|
from_y: from.y,
|
||||||
|
to_x: to.x,
|
||||||
|
to_y: to.y,
|
||||||
|
duration_millis: obj.durationMillis ?? 250,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
case "PressKey":
|
||||||
|
return { kind: "PressKey", key: obj.key };
|
||||||
|
case "Wait":
|
||||||
|
return { kind: "Wait", duration_millis: obj.durationMillis ?? 0 };
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function pointOf(value: unknown): { x: number; y: number } | undefined {
|
||||||
|
if (!value || typeof value !== "object") return undefined;
|
||||||
|
const obj = value as Record<string, unknown>;
|
||||||
|
if (typeof obj.x === "number" && typeof obj.y === "number") {
|
||||||
|
return { x: obj.x, y: obj.y };
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineLockedGlobal("__sanderlingExtractors__", function (): Record<number, unknown> {
|
||||||
|
return evaluateExtractors();
|
||||||
|
});
|
||||||
|
|
||||||
|
defineLockedGlobal("__sanderlingNextAction__", function (): unknown {
|
||||||
|
if (!actionsRoot) return null;
|
||||||
|
// Reset per-tick caches so each invocation gets a fresh DOM scan.
|
||||||
|
randomTapCandidates = null;
|
||||||
|
// Match the goja runtime: retry up to 16 times when a weighted entry's
|
||||||
|
// generator returns []. Otherwise on routes where most generators are
|
||||||
|
// gated to other pages, ~80% of ticks would emit no action.
|
||||||
|
for (let attempt = 0; attempt < 16; attempt++) {
|
||||||
|
const action = serializeAction(resolveGenerator(actionsRoot));
|
||||||
|
if (action !== null) return action;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
|
||||||
|
export {};
|
||||||
Reference in new issue
Block a user