browser tests run only by hand: the suites that launch Chrome / Chromium move to tests/browser/*.browser.ts (bun test, ci, ci:quick and the Forgejo workflows never pick them up; bun run test:browser runs them); the desktop boot check leaves bun run ci (it stays in desktop-release.ts mac at release time); CI=full is gone: Forgejo's ci runs the Rust steps when the push changed a Rust input (STOREBOUND_CI_BASE=HEAD~1), no Playwright install; CONTRIBUTING
This commit is contained in:
parent
4c7ff89950
commit
cd1e13c1e2
20 changed files with 116 additions and 133 deletions
|
|
@ -1,5 +1,4 @@
|
|||
# `bun run ci` after .forgejo/actions/linux-setup: the target / timings / Playwright caches, bun install, headless
|
||||
# Chromium, Node 20 (the npm bundle's oldest supported Node) and the full CI. Shared by ci.yml and release.yml.
|
||||
# `bun run ci` after .forgejo/actions/linux-setup: the target / timings caches, bun install, Node 20 (the npm bundle's oldest supported Node) and the full CI. Shared by ci.yml and release.yml.
|
||||
name: ci-run
|
||||
description: storebound's full CI (bun run ci) on the Linux runner
|
||||
runs:
|
||||
|
|
@ -21,25 +20,12 @@ runs:
|
|||
run: |
|
||||
bun --version && cargo --version
|
||||
bun install --frozen-lockfile
|
||||
# Playwright's Chromium and headless shell (~280 MB), keyed by the browser builds the installed playwright-core
|
||||
# wants (its browsers.json); the system libraries and fonts (--with-deps) are installed on every run, the job
|
||||
# container starts clean
|
||||
- uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-${{ runner.os }}-${{ hashFiles('node_modules/playwright-core/browsers.json') }}
|
||||
restore-keys: playwright-${{ runner.os }}-
|
||||
# the job runs as root in a container: storebound adds --no-sandbox to the Chromium it starts itself there
|
||||
# (cli/shots/browser.ts sandboxArgs), Playwright's own launches already pass it
|
||||
- name: Headless Chromium (browser tests, desktop boot check)
|
||||
shell: bash
|
||||
run: bunx playwright install --with-deps chromium
|
||||
# the npm bundle runs on Node 20+: tools/ci.ts smoke-tests dist/storebound.mjs with the oldest supported Node
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
# the runner sets CI=true in the job and it wins over the workflow env above: set CI=full for the command
|
||||
# itself, or tools/ci.ts skips the desktop Rust tests and clippy when the push did not touch src-tauri
|
||||
# no browser tests in CI (tests/browser/ runs only with `bun run test:browser`); the changes of this push (against
|
||||
# its parent; the checkout has fetch-depth 2) decide the Rust steps, as on a laptop against origin/main
|
||||
- name: bun run ci
|
||||
shell: bash
|
||||
run: CI=full bun run ci
|
||||
run: STOREBOUND_CI_BASE=HEAD~1 bun run ci
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# Every push to main is a release candidate (docs/desktop-release.md, "Release candidates"). In parallel:
|
||||
# ci bun run ci (adapters, desktop version, tests, typecheck, desktop i18n / typecheck / build, the boot check
|
||||
# in headless Chromium, desktop Rust tests and clippy, scrub); tools/ci.ts runs it as a task graph with
|
||||
# parallel test shards on the one Linux runner. CI=full runs the Rust steps on every push.
|
||||
# parallel test shards on the one Linux runner; no browser test runs here (tests/browser/, `bun run
|
||||
# test:browser` by hand). The Rust steps run when the push changed a Rust input (STOREBOUND_CI_BASE).
|
||||
# acceptance the release acceptance matrix (tools/acceptance.ts): every user-facing command from the packed npm
|
||||
# tarball with real piped stdin, Linux and Windows x Node 20 / Node 22 / Bun.
|
||||
# linux AppImage + deb (path-free build), self-tests, both started under Xvfb, privacy scan.
|
||||
|
|
@ -26,7 +27,6 @@ env:
|
|||
BUN_VERSION: '1.4.0'
|
||||
CARGO_TERM_COLOR: never
|
||||
CARGO_INCREMENTAL: '0'
|
||||
CI: full
|
||||
|
||||
jobs:
|
||||
gate:
|
||||
|
|
@ -63,6 +63,8 @@ jobs:
|
|||
timeout-minutes: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 2
|
||||
- uses: ./.forgejo/actions/linux-setup
|
||||
with:
|
||||
cache-key: ci
|
||||
|
|
|
|||
|
|
@ -9,9 +9,16 @@ but fixes, store rule updates and new engines / AI tools are very welcome.
|
|||
bun install # Bun 1.1+; playwright is a dev dependency (renders, web capture)
|
||||
bun run ci:quick # inner loop: typecheck + the tests your change touches + scrub (seconds)
|
||||
bun run ci # full gate before pushing: every check and test (adapters, API tables, typecheck,
|
||||
# desktop build and boot check, Rust host when it changed, scrub)
|
||||
# desktop build, Rust host when it changed, scrub); no browser
|
||||
bun run test:browser # only by hand: the suites that start Chrome / Chromium (tests/browser/*.browser.ts)
|
||||
```
|
||||
|
||||
**Browser tests run only by hand.** The suites that launch a browser (console fill / flow / session / persist, web
|
||||
capture, glyphs and ligatures, local fonts, feature-graphic fit, site overflow) live in `tests/browser/` as
|
||||
`*.browser.ts`, which `bun test`, `ci`, `ci:quick` and the Forgejo workflows never pick up. Run `bun run test:browser`
|
||||
when you change that code. The one browser step left in any automatic path is the desktop boot check inside
|
||||
`tools/desktop-release.ts mac` at release time: the built window must be seen rendering before a build ships.
|
||||
|
||||
**Which one when.** Run `bun run ci:quick` while you work: it diffs against `origin/main` (committed, staged,
|
||||
unstaged and new files), maps the changed files to the tests that import them (directly, through other modules, or
|
||||
by naming a fixture or page) and runs those, plus `tsc` (incremental) and the scrub. A change to `package.json`,
|
||||
|
|
@ -21,10 +28,10 @@ push and check its exit code: it is the gate, `ci:quick` is not.
|
|||
`tools/ci.ts` runs the steps as a task graph (independent steps at the same time, output prefixed with the step
|
||||
name, the first failure stops the rest, a time-per-step summary at the end). The tests run as parallel shards
|
||||
(CPU cores - 1, at most 8; `STOREBOUND_CI_SHARDS=n` overrides) balanced by the durations of the last run, kept in
|
||||
`node_modules/.cache/storebound-ci/`; the browser suites get a shard each. So tests must be shard-safe: temp folders
|
||||
`node_modules/.cache/storebound-ci/`. So tests must be shard-safe: temp folders
|
||||
from `mkdtempSync`, servers on port 0, no fixed sleeps (wait for the event), no state shared through files in the
|
||||
checkout. The desktop Rust host (`cargo test`, `clippy`) runs only when `apps/desktop/src-tauri/` or
|
||||
`tools/build-sidecars.ts` changed against `origin/main`; `CI=full bun run ci` always runs it (Forgejo CI does).
|
||||
`tools/build-sidecars.ts` changed against `origin/main` (Forgejo CI: against the pushed commit's parent).
|
||||
|
||||
Try the screenshot and video pipeline on the demo game without any store account: see
|
||||
[examples/demo-game/README.md](examples/demo-game/README.md).
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ fails without the fix.
|
|||
| 63 | 2026-10 | STAR DASH: re-running `play products` / `prices import` to create 8 new IAPs also changed the live prices of existing products; `--only` was needed. In a terminal (no `--yes`) the one confirmation covered price changes too. | Price changes of existing products need `--allow-price-changes`, TTY or not; the terminal question covers creates and other updates only; with the flag a second question lists each old -> new (asked in a terminal even with `--yes`); dry runs show a "price changes on existing products (held)" section (`allowPriceChanges`, `heldSection` in `cli/lib/price-gate.ts`; `play products`, `prices import`, `play subscriptions`, `apple subscriptions`). `tests/price-gate.test.ts` (TTY and non-TTY). |
|
||||
| 62 | 2026-10 | STAR DASH: `prices ppp` cut high-income markets (Germany EUR 3.59 instead of 5.99 for USD 4.99; UK, France, Japan cut too): it set base x PPP level x exchange rate, a net price, against VAT-inclusive store prices, and cut every region. | Par = the store's own conversion of the US price (Play convertRegionPrices, Apple equalizations; offline base x fx x (1 + VAT) rounded up); share = clamp(PPP level, 0.3, 1); defaults: only decreases, PPP only where the level is <= 0.5, World Bank high-income economies and CN at par, Play skips CN, top-10 revenue market drops > 20 % warned (from `.storebound/reports/` sales reports, else a static list); unchanged regions are left out of the sheet (`shareOf`, `offlinePar`, `planRows`, `topMarketDrops`, `topMarketsFromReports` in `cli/lib/ppp.ts`). `tests/ppp.test.ts`. |
|
||||
| 61 | 2026-10 | STAR DASH: `prices ppp --json` printed log lines on stdout before the JSON, so parsing stdout failed. | `withJsonOutput` (`cli/lib/log.ts`) wraps every `--json` run in `cli/index.ts`: console.log / info go to stderr, `printJson` writes stdout, prompts use `promptOut()`; commands that printed JSON with console.log use `printJson`. `tests/json-stdout.test.ts` runs every command that reads `--json` against the fake stores and parses stdout. |
|
||||
| 55 | 2026-10 | The Play feature graphic (1024 x 500) cut the phone off at the bottom: the game template's phone was 232 px wide at top 36 and rotated 7 deg, so its outline spanned about y 24..552 and the game's bottom bar was lost; the app template bled too, and `shots.feature` only took frame + palette. Nothing measured the device against the canvas. | `featureLayout` / `rotatedBox` (`cli/shots/lib.ts`) size the device so its rotated box fits the canvas with a 24 px margin (`shots.feature.fit` 'contain', the default; 'bleed' keeps the crop on purpose; `scale` 0.5-1.5), both templates use it. The templates record each device's measured layout box (`devices` in report.json) and `shots check` fails a feature graphic whose device leaves the canvas unless fit is 'bleed', and warns for a screenshot's front device off a side or the top (`deviceIssues`, `cli/shots/report.ts`). `tests/feature-fit.test.ts`. |
|
||||
| 55 | 2026-10 | The Play feature graphic (1024 x 500) cut the phone off at the bottom: the game template's phone was 232 px wide at top 36 and rotated 7 deg, so its outline spanned about y 24..552 and the game's bottom bar was lost; the app template bled too, and `shots.feature` only took frame + palette. Nothing measured the device against the canvas. | `featureLayout` / `rotatedBox` (`cli/shots/lib.ts`) size the device so its rotated box fits the canvas with a 24 px margin (`shots.feature.fit` 'contain', the default; 'bleed' keeps the crop on purpose; `scale` 0.5-1.5), both templates use it. The templates record each device's measured layout box (`devices` in report.json) and `shots check` fails a feature graphic whose device leaves the canvas unless fit is 'bleed', and warns for a screenshot's front device off a side or the top (`deviceIssues`, `cli/shots/report.ts`). `tests/browser/feature-fit.browser.ts`. |
|
||||
| 54 | 2026-10 | The second `apple build` of a Capacitor game archived fine, then `xcodebuild -exportArchive` failed with "exportArchive Copy failed" (exit 70). The IDEDistribution log had the real cause: `rsync error: syntax or usage error (code 1)` in IDEDistributionCreateIPAStep, because a Homebrew rsync 3.x, installed and linked that day by another session's release tool, came first on PATH. A tool changed a global binary on a shared machine as a side effect. | `apple build` runs xcodebuild with Xcode's and Apple's tool folders first on PATH (`xcodePath`) and prints a note when a non-Apple rsync is first (`foreignRsync`); an export failure shows the error lines of the newest IDEDistribution*.log (`distributionErrors`, `cli/lib/xcode.ts`, `cli/apple/build.ts`). `storebound doctor` flags a non-Apple rsync first on PATH. `tools/desktop-release.ts publish` calls rsync by absolute path (`/usr/bin/rsync` without `--chmod`, modes set on the server; else the Homebrew keg), never from PATH (`publishSteps`). Rule: tools never install or relink global binaries as a side effect. `tests/apple-build-export.test.ts`. |
|
||||
| 53 | 2026-10 | After that failed export, `apple upload build/ios/export/App.ipa --yes` uploaded nothing: it read the stale IPA of build 1, found build 1 already VALID on App Store Connect and exited 0 ("build 1 is VALID"), so `build && upload` looked green. The export also wrote into the same folder every time. | `apple build` exports each build into its own folder `build/ios/export/<version>-<build>/` (removed first) and exits non-zero on any export failure or a missing IPA (`exportIpa`). `apple upload` fails with exit 1 when the IPA's build number is already on App Store Connect ("this ipa is build 1, which is already on App Store Connect (VALID): did the last build fail?"); only `--allow-existing` re-checks it. `--build N` picks / requires that build's IPA; the release and beta flows use `apple upload --no-wait --build ${BUILD} --allow-existing`, so a resumed flow still passes and a stale IPA never does (`existingBuild`, `exportedIpas`, `cli/apple/upload.ts`; `tests/apple-build-export.test.ts`). |
|
||||
| 52 | 2026-10 | An iOS app that is live on the App Store but whose source is not on this machine could not appear in the desktop: the machine's app list (`~/.storebound/projects.json`) took only folders. | `storebound apps add --store apple\|play --app-id <id> --account <profile>` writes a store-only entry (`storeApps`, no path; `apps folders` shows "no local project", `apps remove apple:<id>`); the desktop adds it to the app list of the account with that profile (`registry::with_store_only`, `store_apps_cached` / `store_apps_refresh`), shown read-only as "Store only"; writing the folder list keeps the entries. `tests/onboard.test.ts`, `src-tauri/tests/machine.rs`, the boot check's `shared-profiles` case (zero uncaught errors). |
|
||||
|
|
@ -31,10 +31,10 @@ fails without the fix.
|
|||
| 44 | 2026-10 | "en-US: no APP_IPHONE_67 screenshots" blocked live apps that have the 6.5" set: App Store Connect requires the 6.9" set or, without it, the 6.5" set (and scales down from it). | `ACCEPTED_INSTEAD` in `cli/lib/validate-rules.ts`: the 6.5" set satisfies the 6.9" requirement (and either 13" iPad type the other); then a recommendation, not a blocker (`tests/wave1.test.ts`, a live-shape set list with only APP_IPHONE_65). |
|
||||
| 43 | 2026-10 | The App Store keyword field is limited to 100 characters, not UTF-8 bytes (entry 4 was wrong): live versions READY_FOR_SALE read on 2026-10-13 hold Thai 95 characters / 265 bytes, Russian 91 / 169, Japanese 49 / 123, Turkish 100 / 105, Polish 99 / 105, and App Store Connect accepted them. | `check` (`cli/lib/copy.ts`), `check --aso` (`auditAppleKeywords`), `listing check` (`checkFields`), `listing translate` (`normalizeKeywords`) and `aso suggest` (`fitKeywordField`) count characters; `tests/listing-translate.test.ts`, `tests/wave1.test.ts`, `tests/aso-plus.test.ts` with Thai, Turkish and Polish fields over 100 bytes. |
|
||||
| 42 | 2026-10 | `apple testflight distribute --build 1 --group Team --yes` failed with 400 on `GET /v1/betaGroups?filter[builds]=`: Apple's spec declares the filter, App Store Connect refuses it, and the spec has no GET of a build's groups. Before the groups existed it said `unknown group "Team" ()`. | Distribute reads each target group's builds (`GET /v1/betaGroups/{id}/relationships/builds`), reports internal all-builds groups as "already available" and never sends them the build; `unknownGroupMessage` points at `groups sync` for configured groups. `APPLE_LIVE_REJECTED` (`cli/api/client.ts`, also refused by `storebound api`); `tests/asc-query-guard.test.ts` checks every App Store Connect GET with a query in `cli/` against the spec's declared parameters and that list; the fake App Store Connect answers 400 to them (`tests/idempotency.test.ts`). |
|
||||
| 41 | 2026-10 | Every Play App content card ended in an owner-only Save, so the owner came back once per card (privacy policy, ads, app access, advertising ID, government, financial, health, target audience, content rating, data safety). Saving an App content declaration is not public: it goes to Google only with the next "Send changes for review" in Publishing overview (verified 2026-10-13). | Card Saves are the agent's (`declaration-save`): `declarationSaveAllowed` (exactly Save / Kaydet, a button on an /app-content/<card> page) and `flowAllowed` for content rating and target audience, while `publishingControl` refuses "Send changes for review", "Submit N changes for review", Submit, Publish, "Restart review" everywhere; the plans' `buttons.final` is "Send changes for review"; `console fill` presses the declaration's Save; `console plan play-app-content --all` walks every card and ends with one owner list (`cli/lib/console-forms.ts`, `cli/lib/console-plan.ts`, `cli/lib/console-play-plans.ts`, `cli/console/fill.ts`; `tests/console-play-plans.test.ts`, `tests/console-flow.test.ts`). |
|
||||
| 41 | 2026-10 | Every Play App content card ended in an owner-only Save, so the owner came back once per card (privacy policy, ads, app access, advertising ID, government, financial, health, target audience, content rating, data safety). Saving an App content declaration is not public: it goes to Google only with the next "Send changes for review" in Publishing overview (verified 2026-10-13). | Card Saves are the agent's (`declaration-save`): `declarationSaveAllowed` (exactly Save / Kaydet, a button on an /app-content/<card> page) and `flowAllowed` for content rating and target audience, while `publishingControl` refuses "Send changes for review", "Submit N changes for review", Submit, Publish, "Restart review" everywhere; the plans' `buttons.final` is "Send changes for review"; `console fill` presses the declaration's Save; `console plan play-app-content --all` walks every card and ends with one owner list (`cli/lib/console-forms.ts`, `cli/lib/console-plan.ts`, `cli/lib/console-play-plans.ts`, `cli/console/fill.ts`; `tests/console-play-plans.test.ts`, `tests/browser/console-flow.browser.ts`). |
|
||||
| 40 | 2026-10 | Finding a new app's Play Console app id (`play.appId`) took several steps: the app list shows 10 apps per page and the new app was not on page 1. The id was in the URL Create app redirects to (/developers/<devId>/app/<appId>/app-dashboard) all along. | `console verify play-app-record --url <url>` reads the id from the redirect (or the capture JSON's url), shows the `play.appId` change as a dry diff and `--write` upserts it (`upsertNestedString`); `storebound play app-id` finds it by package name across captured app list pages (search box or pages) as an agent step (`cli/lib/play-app-id.ts`, `cli/play/app-id.ts`; `tests/play-app-id.test.ts`, a modeled 3-page list). |
|
||||
| 39 | 2026-10 | Play IARC questionnaire: page 1's Next did nothing until the IARC Terms of Use box was ticked, an owner-only agreement, so the agent stopped mid-run and waited for the owner. | The box is in the plan's ONE owner batch before the run with its exact place (`ownerBatch`, printed first; `play-app-content` and `play-content-rating`); the step says not to press Next again and again; `console fill` waits for the tick ("agreement needed on …", `blocksNext`) instead of pressing a Next that does nothing (`tests/console-play-plans.test.ts`, `tests/console-flow.test.ts`). |
|
||||
| 38 | 2026-10 | App Store Connect's New App dialog: a programmatic value set (a form_input-style set) on the Name and SKU textboxes left the dialog's React state empty (""), while clicking the field and typing worked. | The `apple-app-record` plan says "type, don't set value" (uiFacts and the fill step); `console fill` types every text field key by key and reads it back (`typeText` in `cli/console/fill.ts`); `tests/console-flow.test.ts` (a New App fixture whose state follows key presses only). |
|
||||
| 39 | 2026-10 | Play IARC questionnaire: page 1's Next did nothing until the IARC Terms of Use box was ticked, an owner-only agreement, so the agent stopped mid-run and waited for the owner. | The box is in the plan's ONE owner batch before the run with its exact place (`ownerBatch`, printed first; `play-app-content` and `play-content-rating`); the step says not to press Next again and again; `console fill` waits for the tick ("agreement needed on …", `blocksNext`) instead of pressing a Next that does nothing (`tests/console-play-plans.test.ts`, `tests/browser/console-flow.browser.ts`). |
|
||||
| 38 | 2026-10 | App Store Connect's New App dialog: a programmatic value set (a form_input-style set) on the Name and SKU textboxes left the dialog's React state empty (""), while clicking the field and typing worked. | The `apple-app-record` plan says "type, don't set value" (uiFacts and the fill step); `console fill` types every text field key by key and reads it back (`typeText` in `cli/console/fill.ts`); `tests/browser/console-flow.browser.ts` (a New App fixture whose state follows key presses only). |
|
||||
| 37 | 2026-10 | A `scan.ignore` entry whose `match` text was in the evidence (a code line) instead of the finding title accepted nothing and said nothing. `init --profile game` wrote example-looking values into `.storebound/credentials.json` (`YOUR_KEY_ID`, `/absolute/path/to/upload.p12`, `sk_your_revenuecat_v2_secret_key`), which commands read as real credentials. | `scan.ignore[].evidence` matches an evidence line ("file:line text"), `match` stays the title; `scan` (report notes) and `check` (warning) name every entry that accepts no finding (`ignoreMatches` / `applyIgnore` / `unusedIgnores` in `cli/lib/scan/rules.ts`). `templates/credentials.example.json` holds empty strings only; `isPlaceholder` (`cli/lib/credentials.ts`) makes "your-…", `<…>`, "xxx", "/absolute/path/to/…", "example" and the zero Apple Ads client id missing in every credential read; `check` warns, `doctor` shows set / missing / placeholder per key. `tests/game-report.test.ts`. |
|
||||
| 36 | 2026-10 | Privacy answers derived from the wrong source: `privacy-manifest --propose` wrote an EMPTY NSPrivacyCollectedDataTypes while `appPrivacy.types` listed 5 collected types; `console plan apple-app-privacy` added "Other Usage Data" from dataSafety (PSL_OTHER_APP_ACTIVITY) although `appPrivacy.types` was given; `console plan play-advertising-id` answered "uses advertising ID" Yes because dataSafety declared PSL_DEVICE_ID (a random install id, not the advertising ID). | `--propose` builds NSPrivacyCollectedDataTypes from `appPrivacy.types` (types, linked, tracking, purposes as Apple's manifest keys; NSPrivacyTracking still only with app-level domains, lesson 29) and `check` warns on an empty list against collected types. `appPrivacy.types` is authoritative in the App Privacy sheet (unlisted = not collected, certain); without it, tracking false and no ad / attribution SDK make Advertising Data, Customer Support, Environment Scanning, Hands and Head "not collected" unless a project signal suggests them (`cli/lib/project-signals.ts`). Advertising ID comes from ad / attribution SDKs, the AD_ID permission (source or merged manifest, or its `tools:node="remove"`), ads.appAdsTxt or `console.appContent.advertisingId`; none: No (certain), a conflict: ask (`advertisingIdUse`). `tests/game-report.test.ts`. |
|
||||
| 35 | 2026-10 | `scan` on a Capacitor game: rule `review-prompt` (3.2.2(x), reward for rating) fired on `const rate = F.genRate(lv, bonus.gen(r));` (a bare "rate" next to "bonus"); every account finding cited `import './buildings/register';` as sign-up evidence; and 5.1.1(v) cleared as soon as `accounts.inApp` named a screen the code did not have. | The incentive signal needs a rate-us / leave-a-review phrase next to a reward, a reward "for rating / reviewing", or a reward granted within a few lines of a system review prompt call (`rewardNearReviewApi`). Sign-in / sign-up / deletion signals skip import, require and export-from lines (`codeLine`), and auth endpoints must be an absolute path, URL or `${base}/...`, never a module path; config-only accounts say "from config (accounts.enabled)". A config claim of in-app deletion without code evidence stays a finding (medium, "claimed by config, not found in code"). `tests/game-report.test.ts` with `tests/fixtures/scan/capacitor-game`. |
|
||||
|
|
@ -52,21 +52,21 @@ fails without the fix.
|
|||
| 23 | 2026-10 | Capacitor: native build ships old web assets. `flow run release --apply` went straight into `apple build` (xcodebuild archive) with `ios/App/App/public` and `android/app/src/main/assets/public` 3.5 hours older than the latest source commit: the native projects hold the copy `npx cap sync` made last, and nothing ran the web build + cap sync first, so the old game would have shipped. | New `build.prebuild` (per store `apple.prebuild` / `play.prebuild`): `apple build` / `play build` run it first (printed, streamed, nonzero exit stops; `--dry` only prints; `--no-prebuild`; once per `flow run` through `STOREBOUND_PREBUILD_DONE`). After it, a Capacitor project's copied `public/index.html` must be newer than the last commit outside ios/ android/ node_modules/ store/ .storebound/ and `<webDir>/index.html`, else the build stops ("web assets are older than the source ..."; `--allow-stale-web` overrides). `check` warns when stale without a prebuild; build output and dry plan show the bundle's sha256 + mtime. Monorepo (`apps/native` with its own capacitor.config.ts, android/, ios/; `play build` failed with "no Gradle project ... in android or ."): the one Capacitor project one or two levels down is found and named, several ask for `play.build.dir` / `apple.build.*` (`cli/lib/web-assets.ts`, `tests/web-assets.test.ts`). |
|
||||
| 22 | 2026-10 | `play build` built a good AAB (Capacitor 8 / AGP), then failed its own check with "AAB manifest without package / versionCode". The aapt2 proto manifest was read with element field numbers one too high (name 4, attribute 5, child 6 instead of Resources.proto's 3, 4, 5). The test fixture came from the same wrong encoder, so the tests passed. | `aabInfo` / `encodeProtoManifest` in `cli/lib/gradle.ts` follow Resources.proto. `tests/build-sign.test.ts` builds the manifest bytes by tag, without the encoder, and the parser was checked against real AGP bundles. Lesson: a parser's fixture must not come from the same code's encoder. |
|
||||
| 21 | 2026-10 | Play Data safety: conditional required rows. `play data-safety` for a game with username accounts (`accountCreation: ['PSL_ACM_USER_ID_OTHER_AUTH']`) got 400 "Invalid safety labels declaration: Response missing for PSL_ACCOUNT_DELETION_URL": once account creation is not `PSL_ACM_NONE`, Play requires the delete-account URL and the data-only deletion question (`PSL_SUPPORT_DATA_DELETION_BY_USER`, and `PSL_DATA_DELETION_URL` when it is yes), and refuses `PSL_HAS_OUTSIDE_APP_ACCOUNTS`; storebound wrote none of them. Later (2026-10-12) a game got 400 "Line 7: Response ID missing" for `PSL_SUPPORT_DATA_DELETION_BY_USER,,FALSE`: the current form asks it as a single choice (`DATA_DELETION_YES` / `DATA_DELETION_NO` / `DATA_DELETION_NO_AUTO_DELETED`) and no longer has `PSL_DATA_COLLECTION_USER_REQUEST_DELETE`; the CSV must use the current form's Response IDs. | `dataSafetyRows` writes them from `accounts.deletionUrl` and `dataSafety.partialDeletion` / `partialDeletionUrl`; `DATA_SAFETY_RULES` / `typeRules` / `dataSafetyIssues` in `cli/lib/data-safety.ts` check every conditional row in `check`, `project.declarations` and `play data-safety` (dry and real, before the POST); `tests/pure.test.ts` (conditional required rows), `tests/idempotency.test.ts`, `tests/app-privacy.test.ts`. The single choice is written one row per option (`partialDeletion: true | false | 'auto90'`); every (Question ID, Response ID) pair, `dataSafety.extra` included, is checked against Google's empty form exported from Play Console (`cli/data/play-data-safety-form.csv`, 2026-10-12; refresh: App content > Data safety > Export to CSV, nothing answered) by `formPairIssues`, and each Answer requirement comes from it; `tests/pure.test.ts` (pairs from Play's exported form). |
|
||||
| 20 | 2026-10 | Hours of selector-fix cycles of the scripted console engine (`console fill`): the real console kept differing from the model (App Privacy "Set Up" as plain text, purpose checkboxes without an accessible name, two information steps in a type dialog, Play's material radios that ignore the native input). | Agent-first console mode: `console plan <form>` gives an AI agent the start URL, steps, answer sheet with the console's labels, never-press list and verified UI facts; the agent drives the page with its own browser tool and stops before the final button; `console verify <form>` checks the result (App Privacy summaries from the page text, Play Create app from input states, API for the App Store record and age rating). The scripted fill stays a fast path (and now handles the verified facts). MCP: `console_plan`, `console_verify` (read-only). `tests/console-plan.test.ts`, `tests/console-flow.test.ts`. |
|
||||
| 20 | 2026-10 | Hours of selector-fix cycles of the scripted console engine (`console fill`): the real console kept differing from the model (App Privacy "Set Up" as plain text, purpose checkboxes without an accessible name, two information steps in a type dialog, Play's material radios that ignore the native input). | Agent-first console mode: `console plan <form>` gives an AI agent the start URL, steps, answer sheet with the console's labels, never-press list and verified UI facts; the agent drives the page with its own browser tool and stops before the final button; `console verify <form>` checks the result (App Privacy summaries from the page text, Play Create app from input states, API for the App Store record and age rating). The scripted fill stays a fast path (and now handles the verified facts). MCP: `console_plan`, `console_verify` (read-only). `tests/console-plan.test.ts`, `tests/browser/console-flow.browser.ts`. |
|
||||
| 19 | 2026-10 | `play images --dry` aborted with "en-US: no sevenInchScreenshots ... (render first)" on a phone-only project: Play's 7" and 10" tablet screenshots are optional (required: 512 icon, 1024x500 feature graphic, at least 2 screenshots; Play Console Help answer 9866151), but every image type was treated as required. | `play images` uploads what exists (icon, feature graphic, phone screenshots, tablet / TV / Wear only when rendered), prints an info line for missing optional sets, never deletes what Play has for them without `--prune`; `play.tablets` (default: tablet renders exist) switches tablets for `shots render` / `shots check` / `shots gallery` / `validate` / `play images`, and `true` with missing renders is an error; then a 404 "Listing for language 'de-DE' not found." for languages still `status=draft`: `play images` takes the languages `play listing` pushes and skips one with no Play listing as info (`noListing`) (`cli/play/images.ts` `langImages`, `resolvedTablets` in `cli/shots/report.ts`, `tests/play-post-reads.test.ts`). |
|
||||
| 18 | 2026-10 | `console fill play-app-record` with a copy of the owner's Chrome cookies reached the Play account chooser once, then Google answered `/v3/signin/rejected` and the chooser: Google session cookies are device-bound (Chrome's Device Bound Session Credentials) and Google signs out sessions whose cookies look stolen, which can also sign the owner out of their real Chrome. | Never copy Google cookies by default: storebound's own profile is the default (the owner signs in once by hand), started by storebound as a normal Chrome (no `--enable-automation`, `navigator.webdriver` false) and attached over CDP; `--use-chrome-session google\|apple` is an explicit opt-in that warns; `/signin/rejected` is said plainly (`cli/console/session.ts`, `tests/console-session.test.ts`). Confirmed 2026-10-12: the earlier default copy DID sign the owner out of Play Console in their real Chrome. |
|
||||
| 17 | 2026-10 | After the data types were saved, the real App Privacy page shows each type's "Set Up <type>" as a plain `<p>` in a div with a click handler (no button role, no link) and puts the types in a box after the Data Types heading's box: role-based locators and the "nearest heading" check found nothing, so no type was set up. A "?" (More information) button sits next to each type. | `setUpAllowed` / `setUpTexts` (`cli/lib/console-forms.ts`: exact text "Set Up <type>" of a type storebound answers, in the Data Types section, outside dialogs) and `setUpClick` / `setUpType` / `flowWhere` (last heading before the control) in `cli/console/fill.ts`; the per-type dialog steps are `page` sections, so an unknown step dumps and pauses instead of ending the run; `tests/fixtures/console/apple-app-privacy-setup.html` (structure of the real page) and `tests/console-flow.test.ts`. |
|
||||
| 16 | 2026-10 | `console fill play-app-record` with the copied Chrome cookies printed only "still waiting for the sign-in" for 5+ minutes on a signed-in Play Console: Google pages embed `accounts.google.com` frames (the account widget, cookie rotation), and the detector counted any frame on a sign-in host. | `signInReason`: a sign-in page only by its top-level URL, a visible password / email-entry form in the main frame, or a visible Apple sign-in frame (idmsa / appleid); other frames never count. Each detection logs "sign-in detected because: <rule> at <url>"; heartbeats name the page; a copied session that lands on a sign-in URL is said plainly ("did not carry over"); the watchdog dumps the sign-in wait once after `--step-timeout`, `--debug-dump` up to 3 times (`tests/console-fill.test.ts`, chooser / home with the widget frames, a top-level accounts.google.com page). |
|
||||
| 15 | 2026-10 | The real App Privacy "Data Collection" dialog names each data type checkbox by its label run together with its help text ("NameIncluding first or last name"), and labels some types differently from Apple's help page ("Purchases", "Other Data"): exact-name matching found none. | `labelNames` (exact name, then the label as a prefix that ends where the help text starts) and `TYPE_ALIASES` in `cli/lib/console-forms.ts`; the flow fixture mirrors the real dialog's structure (`tests/console-flow.test.ts`). |
|
||||
| 14 | 2026-10 | `console fill apple-app-privacy` clicked Next onto a real console page it did not recognise and then waited 8 hours without a single log line. | Every wait of the console engine is bounded: a step watchdog (`--step-timeout`, 30 s) writes a sanitized debug dump to `.storebound/console-debug/` and prints the page's buttons and the manual steps; a line a minute while waiting; no progress for 20 min stops the run with exit 1; `--debug-dump` dumps every step (`cli/console/debug.ts`, `tests/console-flow.test.ts`, which also scans the engine for waits without a deadline). |
|
||||
| 18 | 2026-10 | `console fill play-app-record` with a copy of the owner's Chrome cookies reached the Play account chooser once, then Google answered `/v3/signin/rejected` and the chooser: Google session cookies are device-bound (Chrome's Device Bound Session Credentials) and Google signs out sessions whose cookies look stolen, which can also sign the owner out of their real Chrome. | Never copy Google cookies by default: storebound's own profile is the default (the owner signs in once by hand), started by storebound as a normal Chrome (no `--enable-automation`, `navigator.webdriver` false) and attached over CDP; `--use-chrome-session google\|apple` is an explicit opt-in that warns; `/signin/rejected` is said plainly (`cli/console/session.ts`, `tests/browser/console-session.browser.ts`). Confirmed 2026-10-12: the earlier default copy DID sign the owner out of Play Console in their real Chrome. |
|
||||
| 17 | 2026-10 | After the data types were saved, the real App Privacy page shows each type's "Set Up <type>" as a plain `<p>` in a div with a click handler (no button role, no link) and puts the types in a box after the Data Types heading's box: role-based locators and the "nearest heading" check found nothing, so no type was set up. A "?" (More information) button sits next to each type. | `setUpAllowed` / `setUpTexts` (`cli/lib/console-forms.ts`: exact text "Set Up <type>" of a type storebound answers, in the Data Types section, outside dialogs) and `setUpClick` / `setUpType` / `flowWhere` (last heading before the control) in `cli/console/fill.ts`; the per-type dialog steps are `page` sections, so an unknown step dumps and pauses instead of ending the run; `tests/fixtures/console/apple-app-privacy-setup.html` (structure of the real page) and `tests/browser/console-flow.browser.ts`. |
|
||||
| 16 | 2026-10 | `console fill play-app-record` with the copied Chrome cookies printed only "still waiting for the sign-in" for 5+ minutes on a signed-in Play Console: Google pages embed `accounts.google.com` frames (the account widget, cookie rotation), and the detector counted any frame on a sign-in host. | `signInReason`: a sign-in page only by its top-level URL, a visible password / email-entry form in the main frame, or a visible Apple sign-in frame (idmsa / appleid); other frames never count. Each detection logs "sign-in detected because: <rule> at <url>"; heartbeats name the page; a copied session that lands on a sign-in URL is said plainly ("did not carry over"); the watchdog dumps the sign-in wait once after `--step-timeout`, `--debug-dump` up to 3 times (`tests/browser/console-fill.browser.ts`, chooser / home with the widget frames, a top-level accounts.google.com page). |
|
||||
| 15 | 2026-10 | The real App Privacy "Data Collection" dialog names each data type checkbox by its label run together with its help text ("NameIncluding first or last name"), and labels some types differently from Apple's help page ("Purchases", "Other Data"): exact-name matching found none. | `labelNames` (exact name, then the label as a prefix that ends where the help text starts) and `TYPE_ALIASES` in `cli/lib/console-forms.ts`; the flow fixture mirrors the real dialog's structure (`tests/browser/console-flow.browser.ts`). |
|
||||
| 14 | 2026-10 | `console fill apple-app-privacy` clicked Next onto a real console page it did not recognise and then waited 8 hours without a single log line. | Every wait of the console engine is bounded: a step watchdog (`--step-timeout`, 30 s) writes a sanitized debug dump to `.storebound/console-debug/` and prints the page's buttons and the manual steps; a line a minute while waiting; no progress for 20 min stops the run with exit 1; `--debug-dump` dumps every step (`cli/console/debug.ts`, `tests/browser/console-flow.browser.ts`, which also scans the engine for waits without a deadline). |
|
||||
| 13 | 2026-10 | App Privacy purposes were over-mapped: Play "advertising" became both Third-Party Advertising and Developer's Advertising or Marketing (Device ID, Coarse Location, Product Interaction, Performance Data). | `applePurpose` maps one Play purpose to one Apple purpose (advertising: third-party only); Developer's Advertising only from `appPrivacy.types`; `check` compares `appPrivacy.types` with Info.plist / PrivacyInfo.xcprivacy (`tests/app-privacy.test.ts`). |
|
||||
| 12 | 2026-10 | A new Play app has no API: Play Console's "Create app" is the only way, its page-level button carries the same words as the form's final submit, and Free cannot become Paid later. | `console fill play-app-record`: `createAppAllowed` lets the opener through only on the app list, never in a form or dialog (EN + TR); declarations never ticked; Free / Paid outlined as an owner check (`tests/console-fill.test.ts`). Verified 2026-10-12: the form is at `/developers/<id>/create-new-app` and has an App package name field with a read-only "Check availability" (filled from `play.packageName`; the package name no longer comes from the first AAB upload); App or game / Free or paid are `<material-radio>`s whose model only updates on a click of the inner `[role=radio]` (the engine clicks it; `console verify play-app-record` checks aria-checked and the "Select …" hints). After Create app the owner adds the new app to the service account (app-level permissions do not cover later apps). |
|
||||
| 12 | 2026-10 | A new Play app has no API: Play Console's "Create app" is the only way, its page-level button carries the same words as the form's final submit, and Free cannot become Paid later. | `console fill play-app-record`: `createAppAllowed` lets the opener through only on the app list, never in a form or dialog (EN + TR); declarations never ticked; Free / Paid outlined as an owner check (`tests/browser/console-fill.browser.ts`). Verified 2026-10-12: the form is at `/developers/<id>/create-new-app` and has an App package name field with a read-only "Check availability" (filled from `play.packageName`; the package name no longer comes from the first AAB upload); App or game / Free or paid are `<material-radio>`s whose model only updates on a click of the inner `[role=radio]` (the engine clicks it; `console verify play-app-record` checks aria-checked and the "Select …" hints). After Create app the owner adds the new app to the service account (app-level permissions do not cover later apps). |
|
||||
| 11 | 2026-10 | Google Play has no public search API, so there is no Play keyword rank or Play competitor list to read without scraping. | `aso` reads App Store search only (iTunes Search API); `aso audit` covers Play through the listing text (short description, density); the aso-basics skill says so. |
|
||||
| 10 | 2026-10 | The iTunes Search API order is close to, not equal to, App Store search (no personalisation, no search ads, no in-app event rows) and it has no subtitle field. | `aso rank` / `aso competitors` / `aso audit` label ranks as a trend and show no competitor subtitles; popularity only from Apple Ads, never estimated (`cli/lib/aso-intel.ts`). |
|
||||
| 9 | 2026-10 | A retried or repeated write can duplicate on a live store: a POST create retried after a timeout or 5xx, a re-run during the list lag right after a create, two runs at once, a Play edit redone after an unclear commit, a crash in the middle of a screenshot set. | Every write is an upsert (`cli/lib/upsert.ts`: findOrCreate with a fresh re-check and a journal of recent creates, createOnce treating 409 as found, no blind retry of a create); API clients retry only requests that are safe twice; Play edits are all or nothing; asset sets keep the matching prefix; one lock per app and store (`cli/lib/lock.ts`, `--force-unlock`). Table: `docs/idempotency.md`; `tests/idempotency.test.ts` runs every write twice and asserts zero writes on the second run. |
|
||||
| 8 | 2026-10 | Capacitor plugins in a monorepo live in `apps/native/node_modules`, so `scan` missed them. | `scan` / `privacy-manifest` resolve workspace packages (`cli/lib/scan/project.ts`, `cli/lib/node-packages.ts`). |
|
||||
| 7 | 2026-10 | Console UIs: the App Store Connect sign-in runs in an `idmsa.apple.com` iframe; "User Access" is a plain heading, not a labeled group; Chrome drops session cookies without `--restore-last-session`. | `console` session and fill (`cli/console/`), `tests/console-*.test.ts`. |
|
||||
| 6 | 2026-10 | Screenshot fonts: only part of the glyphs a language needs were available, so Turkish / Polish / Vietnamese characters fell back to another font silently. | `shots fonts` / `shots check` check every language's characters against the fonts (`tests/glyphs.test.ts`). |
|
||||
| 6 | 2026-10 | Screenshot fonts: only part of the glyphs a language needs were available, so Turkish / Polish / Vietnamese characters fell back to another font silently. | `shots fonts` / `shots check` check every language's characters against the fonts (`tests/browser/glyphs.browser.ts`). |
|
||||
| 5 | 2026-10 | Account features do not imply email collection; Data safety said "email collected" without evidence. | `scan` marks email only with code evidence, else "ask owner" (`cli/lib/scan/declare.ts`). |
|
||||
| 4 | 2026-10 | (Superseded by 43: the limit counts characters.) The App Store keyword limit (100) was taken to count UTF-8 bytes. | See 43. |
|
||||
| 3 | 2026-10 | App Store app names are unique per storefront: "already being used" although the developer owns no such app. | `apple.name` (App Store name with a suffix the owner picks); `app.name` stays the brand. |
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
"smoke:node": "bun tools/node-smoke.ts",
|
||||
"ci": "bun tools/ci.ts",
|
||||
"ci:quick": "bun tools/ci.ts --quick",
|
||||
"test:browser": "bun test --timeout=60000 --max-concurrency=2 ./tests/browser/*.browser.ts",
|
||||
"check": "bun run typecheck && bun test",
|
||||
"prepack": "bun tools/build-node.ts"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ testers) and answers, in ONE batch at the start, only the facts no file proves (
|
|||
the window showed). `page.json` / `page.html` hold labels and structure only: no values, cookies or tokens; emails,
|
||||
phone numbers and the account area are replaced.
|
||||
- **Guard:** the watchdog in `fillSession` (`--step-timeout`, default 30 s; a heartbeat line a minute; 20 min without
|
||||
progress ends the run with exit 1), `cli/console/debug.ts`, `tests/console-flow.test.ts` (unknown-page fixture and a
|
||||
progress ends the run with exit 1), `cli/console/debug.ts`, `tests/browser/console-flow.browser.ts` (unknown-page fixture and a
|
||||
scan for waits without a deadline).
|
||||
|
||||
## 15. App Privacy data types not found in the real dialog
|
||||
|
|
@ -154,7 +154,7 @@ testers) and answers, in ONE batch at the start, only the facts no file proves (
|
|||
("NameIncluding first or last name"), and some labels differ from Apple's help page ("Purchases", "Other Data").
|
||||
- **Do:** nothing by hand: storebound matches the label as a prefix. If the console changes again, the watchdog
|
||||
(pitfall 14) writes a debug dump: re-run with `--debug-dump` and attach it.
|
||||
- **Guard:** `labelNames` and `TYPE_ALIASES` (`cli/lib/console-forms.ts`), `tests/console-flow.test.ts`.
|
||||
- **Guard:** `labelNames` and `TYPE_ALIASES` (`cli/lib/console-forms.ts`), `tests/browser/console-flow.browser.ts`.
|
||||
|
||||
## 16. "Still waiting for the sign-in" on a console that is signed in
|
||||
- **Symptom:** `console fill` (often with the copied Chrome cookies) prints only "still waiting for the sign-in"
|
||||
|
|
@ -167,7 +167,7 @@ testers) and answers, in ONE batch at the start, only the facts no file proves (
|
|||
without `--use-chrome-session` (pitfall 18). Anything else: re-run with `--debug-dump` (up to 3 dumps of the sign-in wait) and attach the
|
||||
folder after the owner looked at `screen.jpg`.
|
||||
- **Guard:** `signInReason` in `cli/console/fill.ts` (top-level URL, the main frame's password / email-entry form, a
|
||||
visible Apple sign-in frame; no other frame counts), the sign-in watchdog dump, `tests/console-fill.test.ts`.
|
||||
visible Apple sign-in frame; no other frame counts), the sign-in watchdog dump, `tests/browser/console-fill.browser.ts`.
|
||||
|
||||
## 17. App Privacy data types listed but never set up
|
||||
- **Symptom:** after the data types were saved, `console fill apple-app-privacy` stops on the App Privacy page with
|
||||
|
|
@ -178,7 +178,7 @@ testers) and answers, in ONE batch at the start, only the facts no file proves (
|
|||
the "?" help buttons or Publish) and fills that type's dialog step by step. A dialog step it does not know: it writes
|
||||
a debug dump, says `paused on "<type>"`, and goes on once the owner answered that step.
|
||||
- **Guard:** `setUpAllowed` (`cli/lib/console-forms.ts`), `setUpClick` / `setUpType` (`cli/console/fill.ts`),
|
||||
`tests/console-flow.test.ts` with `tests/fixtures/console/apple-app-privacy-setup.html`.
|
||||
`tests/browser/console-flow.browser.ts` with `tests/fixtures/console/apple-app-privacy-setup.html`.
|
||||
|
||||
## 18. Never copy Google session cookies: they are device-bound
|
||||
- **Symptom:** `console fill play-app-record` with the owner's copied Chrome cookies reached the Play account chooser
|
||||
|
|
@ -200,7 +200,7 @@ testers) and answers, in ONE batch at the start, only the facts no file proves (
|
|||
in their real Chrome.
|
||||
- **Guard:** `browserPlan` / `openConsoleBrowser` in `cli/console/session.ts` (no copy without the flag, the copy keeps
|
||||
only the form's own provider), Chrome started by storebound without `--enable-automation` and with
|
||||
`navigator.webdriver` false, `googleRejected`; `tests/console-session.test.ts`.
|
||||
`navigator.webdriver` false, `googleRejected`; `tests/browser/console-session.browser.ts`.
|
||||
- **Sources:** https://blog.google/security/protecting-cookies-with-device-bound-session-credentials/ ,
|
||||
https://developer.chrome.com/docs/web-platform/device-bound-session-credentials ,
|
||||
https://knowledge.workspace.google.com/admin/security/investigate-and-take-action-on-suspicious-session-cookies
|
||||
|
|
@ -515,7 +515,7 @@ that fails without the fix (see CONTRIBUTING.md).
|
|||
component state; clicking the field and typing works (verified 2026-10-13).
|
||||
- **Do:** type, don't set value: click the field, type the text key by key, read it back.
|
||||
- **Guard:** the `apple-app-record` plan (uiFacts, the fill step); `console fill` types every text field (`typeText`);
|
||||
`tests/console-flow.test.ts`.
|
||||
`tests/browser/console-flow.browser.ts`.
|
||||
|
||||
## 39. IARC page 1: Next does nothing until the Terms box is ticked
|
||||
- **Symptom:** on the content rating questionnaire's page 1 Next did nothing, and the agent waited mid-run for the
|
||||
|
|
@ -525,7 +525,7 @@ that fails without the fix (see CONTRIBUTING.md).
|
|||
box" with the exact place (Content ratings > Start questionnaire, page 1). Never tick it yourself; if it is still
|
||||
unticked, say so once and poll it, do not press Next again and again.
|
||||
- **Guard:** `ownerBatch` in `console plan play-content-rating` / `play-app-content` (printed first); `console fill`
|
||||
waits for the tick ("agreement needed on …"); `tests/console-play-plans.test.ts`, `tests/console-flow.test.ts`.
|
||||
waits for the tick ("agreement needed on …"); `tests/console-play-plans.test.ts`, `tests/browser/console-flow.browser.ts`.
|
||||
|
||||
## 40. Finding play.appId in a paginated app list
|
||||
- **Symptom:** finding a new app's Play Console app id took several steps: 10 apps per page, the new app not on page 1.
|
||||
|
|
@ -648,7 +648,7 @@ that fails without the fix (see CONTRIBUTING.md).
|
|||
resizes it. `shots check` fails a feature graphic whose device leaves the canvas (measured from the page's layout
|
||||
at render time, `devices` in report.json), and warns when a screenshot's front device runs off a side or the top.
|
||||
- **Guard:** `featureLayout`, `rotatedBox` (`cli/shots/lib.ts`), `deviceIssues` (`cli/shots/report.ts`),
|
||||
`tests/feature-fit.test.ts` (geometry, both templates rendered at 1024 x 500, a bleeding fixture).
|
||||
`tests/browser/feature-fit.browser.ts` (geometry, both templates rendered at 1024 x 500, a bleeding fixture).
|
||||
|
||||
## 56. "Live on Play" while Google was still reviewing, and 1.0 / 1.0.0 shown as two versions
|
||||
- **Do:** never call a Google Play production release live from the Developer API alone: a release reads `completed`
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ import { spawnSync } from 'node:child_process';
|
|||
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import type { ShipConfig, ShotsCapture } from '../cli/lib/config';
|
||||
import { type CaptureReport, defaultConcurrency, planWeb, pool, progressLine, selectJobs, timingSummary } from '../cli/shots/capture-web';
|
||||
import { BASE } from './helpers/store-mock';
|
||||
import type { ShipConfig, ShotsCapture } from '../../cli/lib/config';
|
||||
import { type CaptureReport, defaultConcurrency, planWeb, pool, progressLine, selectJobs, timingSummary } from '../../cli/shots/capture-web';
|
||||
import { BASE } from '../helpers/store-mock';
|
||||
|
||||
const CLI = join(import.meta.dir, '..', 'cli', 'index.ts');
|
||||
const CLI = join(import.meta.dir, '..', '..', 'cli', 'index.ts');
|
||||
|
||||
describe('capture helpers', () => {
|
||||
test('default concurrency: render min(4, cores/2), capture (GPU-heavy) min(3, cores/4), at least 1', () => {
|
||||
|
|
@ -85,7 +85,7 @@ afterAll(() => server.stop(true));
|
|||
|
||||
// launch only: exiting closes the browser's pipe and it quits on its own (a graceful close of Google Chrome, the
|
||||
// fallback without playwright's Chromium, can take 30 s)
|
||||
const canLaunch = spawnSync('bun', ['-e', `const { launch } = await import(${JSON.stringify(join(import.meta.dir, '..', 'cli', 'shots', 'browser.ts'))}); await launch(${JSON.stringify(join(import.meta.dir, '..'))}, { headless: true }); process.exit(0);`], { timeout: 60_000 }).status === 0;
|
||||
const canLaunch = spawnSync('bun', ['-e', `const { launch } = await import(${JSON.stringify(join(import.meta.dir, '..', '..', 'cli', 'shots', 'browser.ts'))}); await launch(${JSON.stringify(join(import.meta.dir, '..', '..'))}, { headless: true }); process.exit(0);`], { timeout: 60_000 }).status === 0;
|
||||
if (!canLaunch) console.warn('capture-web browser tests skipped: no Chromium could launch (bunx playwright install chromium)');
|
||||
|
||||
/** A temp project with this config; cli() runs `storebound shots capture <args>` in it. */
|
||||
|
|
@ -6,19 +6,19 @@
|
|||
// Speed: the browser tests run BROWSER_CONCURRENCY at a time (scope.concurrent, each with its own contexts) and the
|
||||
// engine's waits are the test ones (TEST_WAITS / TEST_TICK_MS: 20 ms rounds, a missing step ends once its page is
|
||||
// loaded and quiet). SLOW tests still wait out some "not here" timeouts of the engine (navMs, scaled down). The relaunch of storebound's own profile
|
||||
// (session cookies kept) needs a real Chrome: tests/console-persist.test.ts.
|
||||
// (session cookies kept) needs a real Chrome: tests/browser/console-persist.browser.ts.
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test';
|
||||
import { mkdtempSync, readdirSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { matchAccount, openAcross, readAccounts, rememberApple, rememberPlay, withAccountIds } from '../cli/console/accounts';
|
||||
import { createAppHere, engineWaits, fillPage, fillSession, navClick, navigate, resumeAt, run, safeClick, signInPage, signInReason } from '../cli/console/fill';
|
||||
import type { ConsoleBrowser } from '../cli/console/session';
|
||||
import { parseArgs } from '../cli/lib/args';
|
||||
import { type ShipConfig, validateExtras } from '../cli/lib/config';
|
||||
import { AGE_RATING_API, consoleAnswers, consoleResult, fillValue, FORM_IDS, type FormId, formMeta } from '../cli/lib/console-answers';
|
||||
import { consoleReached, CREATE_APP_OPENERS, createAppAllowed, formDef, FORM_OPENERS, forbiddenControl, hasToken, isSignInText, isSignInUrl, navAllowed, navPlan, playDeveloperId, switchAllowed } from '../cli/lib/console-forms';
|
||||
import { AFTER_ALL_MS, AFTER_EACH_MS, launchTestBrowser, logLines, scaled, TEST_TICK_MS, TEST_WAITS, testScope, WaitTimeout } from './helpers/browser';
|
||||
import { matchAccount, openAcross, readAccounts, rememberApple, rememberPlay, withAccountIds } from '../../cli/console/accounts';
|
||||
import { createAppHere, engineWaits, fillPage, fillSession, navClick, navigate, resumeAt, run, safeClick, signInPage, signInReason } from '../../cli/console/fill';
|
||||
import type { ConsoleBrowser } from '../../cli/console/session';
|
||||
import { parseArgs } from '../../cli/lib/args';
|
||||
import { type ShipConfig, validateExtras } from '../../cli/lib/config';
|
||||
import { AGE_RATING_API, consoleAnswers, consoleResult, fillValue, FORM_IDS, type FormId, formMeta } from '../../cli/lib/console-answers';
|
||||
import { consoleReached, CREATE_APP_OPENERS, createAppAllowed, formDef, FORM_OPENERS, forbiddenControl, hasToken, isSignInText, isSignInUrl, navAllowed, navPlan, playDeveloperId, switchAllowed } from '../../cli/lib/console-forms';
|
||||
import { AFTER_ALL_MS, AFTER_EACH_MS, launchTestBrowser, logLines, scaled, TEST_TICK_MS, TEST_WAITS, testScope, WaitTimeout } from '../helpers/browser';
|
||||
|
||||
const config: ShipConfig = {
|
||||
app: { name: 'Example Game', profile: 'game', privacyPolicyUrl: 'https://example.com/privacy', supportUrl: 'https://example.com/support', website: 'https://example.com', contactEmail: 'support@example.com' },
|
||||
|
|
@ -181,7 +181,7 @@ const SLOW_TEST_MS = scaled(90_000);
|
|||
/** Engine waits that are expected to SUCCEED: they return as soon as the element is there, so a long cap costs nothing. */
|
||||
const FOUND_MS = scaled(15_000);
|
||||
|
||||
const FIX = join(import.meta.dir, 'fixtures', 'console');
|
||||
const FIX = join(import.meta.dir, '..', 'fixtures', 'console');
|
||||
/** Console URLs of the navigation plans -> modeled pages (bases: asc = <base>asc, play = <base>play). */
|
||||
const ROUTES: Record<string, string> = {
|
||||
'/asc/apps': 'asc-apps.html', '/asc-tr/apps': 'asc-apps-tr.html', '/asc-noplus/apps': 'asc-apps-noplus.html', '/asc-dup/apps': 'asc-apps-dup.html', '/asc-card/apps': 'asc-apps-card.html',
|
||||
|
|
@ -613,7 +613,7 @@ describe.skipIf(!browser)('sign-in and watching (headless, modeled pages on the
|
|||
expect(await resumeAt(page, plan)).toBeNull();
|
||||
await page.close();
|
||||
}, BROWSER_TEST_MS);
|
||||
// the session-cookie persistence of storebound's own profile: tests/console-persist.test.ts (real Chrome, own shard)
|
||||
// the session-cookie persistence of storebound's own profile: tests/browser/console-persist.browser.ts (real Chrome, own shard)
|
||||
});
|
||||
|
||||
// ---- one login, several developer accounts / teams ----
|
||||
|
|
@ -987,7 +987,7 @@ describe.skipIf(!browser)('play-app-record (headless, modeled pages)', () => {
|
|||
});
|
||||
|
||||
test('ticks are gated on agreements only: help text with "saved" is fine, Developer Program Policies is not; buttons keep the full gate', async () => {
|
||||
const { forbiddenTick, forbiddenControl } = await import('../cli/lib/console-forms');
|
||||
const { forbiddenTick, forbiddenControl } = await import('../../cli/lib/console-forms');
|
||||
const productInteraction = 'Product InteractionSuch as app launches, taps, clicks, scrolling information, music listening data, video views, saved place in a game, video, or song, or other information about how the user interacts with the app';
|
||||
expect(forbiddenTick(productInteraction)).toBe(false);
|
||||
expect(forbiddenTick('NameIncluding first or last name')).toBe(false);
|
||||
|
|
@ -7,15 +7,15 @@ import { afterAll, afterEach, beforeAll, describe, expect, test } from 'bun:test
|
|||
import { mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { engineWaits, fillSession } from '../cli/console/fill';
|
||||
import type { ConsoleBrowser } from '../cli/console/session';
|
||||
import { APPLE_DATA_TYPE_IDS, type AppleDataTypeId, type AppleTypeAnswer } from '../cli/lib/app-privacy';
|
||||
import type { ShipConfig } from '../cli/lib/config';
|
||||
import { type FlowWhere, flowAllowed, flowClass, FORM_BUTTONS, labelNames, setUpAllowed, setUpTexts, TYPE_ALIASES } from '../cli/lib/console-forms';
|
||||
import { APPLE_PRIVACY_TYPES } from '../cli/lib/app-privacy';
|
||||
import { AFTER_ALL_MS, AFTER_EACH_MS, launchTestBrowser, logLines, scaled, TEST_TICK_MS, TEST_WAITS, testScope } from './helpers/browser';
|
||||
import { engineWaits, fillSession } from '../../cli/console/fill';
|
||||
import type { ConsoleBrowser } from '../../cli/console/session';
|
||||
import { APPLE_DATA_TYPE_IDS, type AppleDataTypeId, type AppleTypeAnswer } from '../../cli/lib/app-privacy';
|
||||
import type { ShipConfig } from '../../cli/lib/config';
|
||||
import { type FlowWhere, flowAllowed, flowClass, FORM_BUTTONS, labelNames, setUpAllowed, setUpTexts, TYPE_ALIASES } from '../../cli/lib/console-forms';
|
||||
import { APPLE_PRIVACY_TYPES } from '../../cli/lib/app-privacy';
|
||||
import { AFTER_ALL_MS, AFTER_EACH_MS, launchTestBrowser, logLines, scaled, TEST_TICK_MS, TEST_WAITS, testScope } from '../helpers/browser';
|
||||
|
||||
const FIX = join(import.meta.dir, 'fixtures', 'console');
|
||||
const FIX = join(import.meta.dir, '..', 'fixtures', 'console');
|
||||
const base0: ShipConfig = {
|
||||
app: { name: 'Example Game', profile: 'game', privacyPolicyUrl: 'https://example.com/privacy', supportUrl: 'https://example.com/support', website: 'https://example.com', contactEmail: 'support@example.com' },
|
||||
languages: [{ id: 'en', play: 'en-US', apple: 'en-US' }],
|
||||
|
|
@ -473,7 +473,7 @@ describe.skipIf(!browser)('watchdog: an unknown page after a click never means a
|
|||
});
|
||||
|
||||
describe('every wait of the console engine is bounded (code scan)', () => {
|
||||
const SRC = join(import.meta.dir, '..', 'cli', 'console');
|
||||
const SRC = join(import.meta.dir, '..', '..', 'cli', 'console');
|
||||
const files = readdirSync(SRC).filter(f => f.endsWith('.ts'));
|
||||
/** The text from `open` to its matching `close` (brackets balanced). */
|
||||
const span = (s: string, at: number, open: string, close: string): string => {
|
||||
|
|
@ -8,9 +8,9 @@ import { afterAll, beforeAll, describe, expect, test } from 'bun:test';
|
|||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { type ConsoleBrowser, openConsoleBrowser } from '../cli/console/session';
|
||||
import { installedBrowsers } from '../cli/shots/browser';
|
||||
import { capped, CLOSE_TIMEOUT_MS, LAUNCH_TIMEOUT_MS, persistentBrowser } from './helpers/browser';
|
||||
import { type ConsoleBrowser, openConsoleBrowser } from '../../cli/console/session';
|
||||
import { installedBrowsers } from '../../cli/shots/browser';
|
||||
import { capped, CLOSE_TIMEOUT_MS, LAUNCH_TIMEOUT_MS, persistentBrowser } from '../helpers/browser';
|
||||
|
||||
const real = await persistentBrowser();
|
||||
if (!real.label) console.warn(`console-persist tests skipped: ${real.skipWhy}`);
|
||||
|
|
@ -33,7 +33,7 @@ describe.skipIf(!real.label)(`storebound's own profile (${real.label ?? 'no real
|
|||
for (const store of ['apple', 'play'] as const) {
|
||||
test.concurrent(`${store}: a session cookie survives closing and relaunching storebound's own profile`, async () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), `storebound-profile-${store}-`));
|
||||
const root = join(import.meta.dir, '..');
|
||||
const root = join(import.meta.dir, '..', '..');
|
||||
const { chromium } = await import('playwright');
|
||||
// the resolver's own choice, without a STOREBOUND_CHROME from the environment
|
||||
const installed = installedBrowsers(chromium, { ...process.env, STOREBOUND_CHROME: undefined });
|
||||
|
|
@ -10,9 +10,9 @@ import { afterAll, describe, expect, test } from 'bun:test';
|
|||
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { APPLE_COPY_NOTE, AUTOMATION_SWITCHES, browserPlan, chromeDataDir, consoleChromeArgs, CONSOLE_HOSTS, cookieApplies, copySession, devToolsEndpoint, filterCookies, GOOGLE_COPY_WARNING, GOOGLE_REJECTED, googleRejected, NEVER_COPY, openConsoleBrowser, parseUseChromeSession, pickProfile, PROVIDER_HOSTS, sessionFiles } from '../cli/console/session';
|
||||
import { sandboxArgs } from '../cli/shots/browser';
|
||||
import { capped, CLOSE_TIMEOUT_MS, LAUNCH_TIMEOUT_MS } from './helpers/browser';
|
||||
import { APPLE_COPY_NOTE, AUTOMATION_SWITCHES, browserPlan, chromeDataDir, consoleChromeArgs, CONSOLE_HOSTS, cookieApplies, copySession, devToolsEndpoint, filterCookies, GOOGLE_COPY_WARNING, GOOGLE_REJECTED, googleRejected, NEVER_COPY, openConsoleBrowser, parseUseChromeSession, pickProfile, PROVIDER_HOSTS, sessionFiles } from '../../cli/console/session';
|
||||
import { sandboxArgs } from '../../cli/shots/browser';
|
||||
import { capped, CLOSE_TIMEOUT_MS, LAUNCH_TIMEOUT_MS } from '../helpers/browser';
|
||||
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'sb-session-test-'));
|
||||
afterAll(() => rmSync(scratch, { recursive: true, force: true }));
|
||||
|
|
@ -226,7 +226,7 @@ describe.skipIf(!hasChrome)('copy mode with real Chrome (throwaway profile, loca
|
|||
await seed.close();
|
||||
seed = null;
|
||||
const said: string[] = [];
|
||||
b = await openConsoleBrowser(join(import.meta.dir, '..'), {
|
||||
b = await openConsoleBrowser(join(import.meta.dir, '..', '..'), {
|
||||
store: 'apple', useChromeSession: ['apple'], profileDir: tmp('own-profile'), say: m => said.push(m), chromeDir: src, hosts: ['127.0.0.1'], headless: true, platform: 'darwin', launchTimeoutMs: LAUNCH_TIMEOUT_MS,
|
||||
});
|
||||
expect(b.mode).toBe('copy');
|
||||
|
|
@ -256,7 +256,7 @@ describe.skipIf(!hasChrome)('the console window has no automation markers (real
|
|||
{ headers: { 'content-type': 'text/html' } }) });
|
||||
let b: any = null;
|
||||
try {
|
||||
b = await openConsoleBrowser(join(import.meta.dir, '..'), {
|
||||
b = await openConsoleBrowser(join(import.meta.dir, '..', '..'), {
|
||||
store: 'play', profileDir: tmp('own-webdriver'), say: () => {}, headless: true, platform: 'darwin', chromeDir: tmp('no-chrome-here'), launchTimeoutMs: LAUNCH_TIMEOUT_MS,
|
||||
});
|
||||
expect(b.mode).toBe('profile');
|
||||
|
|
@ -5,10 +5,10 @@
|
|||
import { afterAll, describe, expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { boxOutside, DEFAULT_PHONE, FEATURE_CANVAS, featureLayout, rotatedBox } from '../cli/shots/lib';
|
||||
import { deviceIssues } from '../cli/shots/report';
|
||||
import { validateConfig } from '../cli/lib/config';
|
||||
import { AFTER_ALL_MS, launchTestBrowser, scaled } from './helpers/browser';
|
||||
import { boxOutside, DEFAULT_PHONE, FEATURE_CANVAS, featureLayout, rotatedBox } from '../../cli/shots/lib';
|
||||
import { deviceIssues } from '../../cli/shots/report';
|
||||
import { validateConfig } from '../../cli/lib/config';
|
||||
import { AFTER_ALL_MS, launchTestBrowser, scaled } from '../helpers/browser';
|
||||
|
||||
const close = (a: number, b: number) => expect(Math.abs(a - b)).toBeLessThan(0.01);
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ describe.skipIf(!browser)('feature graphic templates at 1024 x 500 (DOM layout b
|
|||
for (const style of ['game', 'app'] as const) for (const fit of ['contain', 'bleed'] as const) test(`${style}, fit ${fit}`, async () => {
|
||||
const page = await browser.newPage({ viewport: { width: 1024, height: 500 } });
|
||||
try {
|
||||
const html = readFileSync(join(import.meta.dir, '..', 'cli', 'shots', style === 'app' ? 'feature-app.html' : 'feature.html'), 'utf8');
|
||||
const html = readFileSync(join(import.meta.dir, '..', '..', 'cli', 'shots', style === 'app' ? 'feature-app.html' : 'feature.html'), 'utf8');
|
||||
const job = { style, phone: DEFAULT_PHONE, logo: false, appName: 'Demo', fonts: { title: 'X' }, frame: 'a.png',
|
||||
device: featureLayout(style, DEFAULT_PHONE, { fit }),
|
||||
palette: { bg: ['#123456', '#345678', '#567890'], glow: '#ffffff', ink: '#000000', shadow: 'rgba(0,0,0,.4)' } };
|
||||
|
|
@ -6,8 +6,8 @@
|
|||
import { afterAll, describe, expect, test } from 'bun:test';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { measureFonts } from '../cli/shots/glyphs';
|
||||
import { AFTER_ALL_MS, launchTestBrowser, scaled } from './helpers/browser';
|
||||
import { measureFonts } from '../../cli/shots/glyphs';
|
||||
import { AFTER_ALL_MS, launchTestBrowser, scaled } from '../helpers/browser';
|
||||
|
||||
// one browser for the file, launched and killed by the shared helper (bounded, load-aware timeouts); tools/ci-plan.ts
|
||||
// lists this file as HEAVY (the browser shard)
|
||||
|
|
@ -17,7 +17,7 @@ if (!browser) console.warn(`glyph ligature test skipped: no Chromium installed (
|
|||
|
||||
afterAll(() => shared.close(), AFTER_ALL_MS);
|
||||
|
||||
const font = readFileSync(join(import.meta.dir, 'fixtures', 'fonts', 'montserrat-text-subset.woff2')).toString('base64');
|
||||
const font = readFileSync(join(import.meta.dir, '..', 'fixtures', 'fonts', 'montserrat-text-subset.woff2')).toString('base64');
|
||||
const TEXT = 'Outfits, fit, office, affine';
|
||||
|
||||
describe.skipIf(!browser)('shots font usage: ligatures', () => {
|
||||
|
|
@ -5,11 +5,11 @@ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { brotliCompressSync, deflateSync } from 'node:zlib';
|
||||
import { cmapTable, fontCodepoints, missingChars } from '../cli/shots/cmap';
|
||||
import { attribute, type FaceJob, type MeasuredText, fontIssues, fontsCss, fontUsage, GlyphError, planFonts, roleTexts, uncoveredChars } from '../cli/shots/glyphs';
|
||||
import { langTexts } from '../cli/shots/fonts';
|
||||
import { allIssues, scanRenders } from '../cli/shots/report';
|
||||
import { shotFonts } from '../cli/shots/lib';
|
||||
import { cmapTable, fontCodepoints, missingChars } from '../../cli/shots/cmap';
|
||||
import { attribute, type FaceJob, type MeasuredText, fontIssues, fontsCss, fontUsage, GlyphError, planFonts, roleTexts, uncoveredChars } from '../../cli/shots/glyphs';
|
||||
import { langTexts } from '../../cli/shots/fonts';
|
||||
import { allIssues, scanRenders } from '../../cli/shots/report';
|
||||
import { shotFonts } from '../../cli/shots/lib';
|
||||
|
||||
// ---- tiny font builders (only a cmap table: enough for the reader) ----
|
||||
function cmapBytes(cps: number[]): Uint8Array {
|
||||
|
|
@ -6,12 +6,12 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'nod
|
|||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { brotliCompressSync } from 'node:zlib';
|
||||
import { fontCodepoints, fontFamilyName } from '../cli/shots/cmap';
|
||||
import { type FaceJob, fontIssues, fontsCss, fontUsage, type FontManifest, GlyphError, planFonts } from '../cli/shots/glyphs';
|
||||
import { langTexts } from '../cli/shots/fonts';
|
||||
import { faceFilePath, fontsRoute, loadLocalFont, localByFamily, localFontIssues, resolveShotFonts, staleLocalFonts } from '../cli/shots/local-fonts';
|
||||
import { planWeb } from '../cli/shots/capture-web';
|
||||
import { validateConfig } from '../cli/lib/config';
|
||||
import { fontCodepoints, fontFamilyName } from '../../cli/shots/cmap';
|
||||
import { type FaceJob, fontIssues, fontsCss, fontUsage, type FontManifest, GlyphError, planFonts } from '../../cli/shots/glyphs';
|
||||
import { langTexts } from '../../cli/shots/fonts';
|
||||
import { faceFilePath, fontsRoute, loadLocalFont, localByFamily, localFontIssues, resolveShotFonts, staleLocalFonts } from '../../cli/shots/local-fonts';
|
||||
import { planWeb } from '../../cli/shots/capture-web';
|
||||
import { validateConfig } from '../../cli/lib/config';
|
||||
|
||||
// ---- minimal font writer: an sfnt (or WOFF2) with a format 12 cmap and a name table ----
|
||||
function cmapBytes(cps: number[]): Uint8Array {
|
||||
|
|
@ -6,9 +6,9 @@
|
|||
// (tests/helpers/browser.ts). SLOW on purpose: 113 widths x 2 themes, each a resize and a layout read.
|
||||
import { afterAll, afterEach, describe, expect, test } from 'bun:test';
|
||||
import { join } from 'node:path';
|
||||
import { AFTER_ALL_MS, AFTER_EACH_MS, launchTestBrowser, testScope } from './helpers/browser';
|
||||
import { AFTER_ALL_MS, AFTER_EACH_MS, launchTestBrowser, testScope } from '../helpers/browser';
|
||||
|
||||
const page = 'file://' + join(import.meta.dir, '..', 'site', 'index.html');
|
||||
const page = 'file://' + join(import.meta.dir, '..', '..', 'site', 'index.html');
|
||||
|
||||
const shared = await launchTestBrowser('playwright-core');
|
||||
const { browser, skipWhy } = shared;
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
import { describe, expect, test } from 'bun:test';
|
||||
import { balance, deferBrowser, depsOf, GLOBAL, HEAVY, junitTimes, listTests, mergeTimings, relatedTests, rustNeeded, shardCount } from '../tools/ci-plan';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { balance, deferBrowser, depsOf, GLOBAL, HEAVY, junitTimes, listTests, mergeTimings, relatedTests, ROOT, rustNeeded, shardCount } from '../tools/ci-plan';
|
||||
|
||||
describe('tools/ci-plan: browser suites deferred on a busy machine', () => {
|
||||
test('deferred only when the 1-minute load is above 2x the cores, never with CI=full', () => {
|
||||
|
|
@ -48,9 +50,10 @@ describe('tools/ci-plan: test shards', () => {
|
|||
expect(t).toEqual({ 'a.test.ts': 20, 'b.test.ts': 20, 'keep.test.ts': 3, 'new.test.ts': 5 });
|
||||
});
|
||||
|
||||
test('every heavy suite exists and every test file is found', () => {
|
||||
test('the browser suites exist but are never in the CI test list (bun run test:browser only)', () => {
|
||||
const all = listTests();
|
||||
for (const h of HEAVY) expect(all).toContain(h);
|
||||
for (const h of HEAVY) { expect(existsSync(join(ROOT, h))).toBe(true); expect(all).not.toContain(h); }
|
||||
expect(all.some(f => f.startsWith('tests/browser/'))).toBe(false);
|
||||
expect(all).toContain('tests/ci-plan.test.ts');
|
||||
expect(all).toContain('apps/desktop/tests/webview.test.ts');
|
||||
expect(all.some(f => f.includes('node_modules') || f.includes('/fixtures/'))).toBe(false);
|
||||
|
|
@ -73,11 +76,8 @@ describe('tools/ci-plan: related tests (ci:quick)', () => {
|
|||
expect(GLOBAL.test('bunfig.toml')).toBe(true);
|
||||
expect(relatedTests([['docs/nobody', 'reads.md'].join('-')], all)).toEqual([]);
|
||||
});
|
||||
test('a browser suite runs for its own module and its near imports, not for every shared core module', () => {
|
||||
expect(relatedTests(['cli/lib/upsert.ts'], all, undefined, HEAVY)).not.toContain('tests/console-fill.test.ts');
|
||||
expect(relatedTests(['cli/lib/upsert.ts'], all)).toContain('tests/console-fill.test.ts');
|
||||
expect(relatedTests(['cli/console/fill.ts'], all, undefined, HEAVY)).toContain('tests/console-fill.test.ts');
|
||||
expect(relatedTests(['tests/capture-web.test.ts'], all, undefined, HEAVY)).toEqual(['tests/capture-web.test.ts']);
|
||||
test('ci:quick never picks a browser suite, whatever changed', () => {
|
||||
for (const f of ['cli/lib/upsert.ts', 'cli/console/fill.ts', 'package.json']) expect(relatedTests([f], all).some(t => t.startsWith('tests/browser/'))).toBe(false);
|
||||
});
|
||||
test('Rust steps only for the Tauri crate or the sidecar build, or with CI=full / unknown diff', () => {
|
||||
expect(rustNeeded(['cli/index.ts'], {})).toBe(false);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
// Shared plumbing of the headless browser tests (tests/console-fill.test.ts, tests/site-overflow.test.ts,
|
||||
// tests/console-session.test.ts). They used to time out now and then, mostly when the machine was busy:
|
||||
// Shared plumbing of the headless browser tests (tests/browser/console-fill.browser.ts, tests/browser/site-overflow.browser.ts,
|
||||
// tests/browser/console-session.browser.ts). They used to time out now and then, mostly when the machine was busy:
|
||||
// - the afterAll browser close (30 s, "(unnamed)" hook timeout). Cause: Google Chrome (the fallback when
|
||||
// playwright's own Chromium is not downloaded) often hangs in its own shutdown after loading http pages and only
|
||||
// exits when its 10 s teardown watchdog fires ("Teardown watchdog expired" in DEBUG=pw:browser); under load that
|
||||
|
|
@ -89,7 +89,7 @@ const firstLine = (e: unknown) => (e as Error).message.split('\n')[0]!;
|
|||
const notInstalled = (e: unknown) => /Executable doesn't exist|distribution '[^']+' is not found|Failed to launch .* because executable/i.test((e as Error).message);
|
||||
|
||||
/**
|
||||
* The browser the persistent-profile tests (tests/console-persist.test.ts) launch: the same kind `console fill` uses
|
||||
* The browser the persistent-profile tests (tests/browser/console-persist.browser.ts) launch: the same kind `console fill` uses
|
||||
* for storebound's own profile (cli/shots/browser.ts, kind "visible"): real Google Chrome (channel "chrome"), else
|
||||
* Playwright's FULL Chromium (channel "chromium"). Never the fast headless shell launchTestBrowser uses: it ignores
|
||||
* --restore-last-session, so session cookies are dropped on relaunch. `label` is null when neither is installed.
|
||||
|
|
|
|||
|
|
@ -13,9 +13,9 @@ export const TIMINGS = join(ROOT, 'node_modules/.cache/storebound-ci/test-timing
|
|||
* never next to the light tests, and those shards start first: eight browsers at once (one per shard) on a machine
|
||||
* other sessions already load to 100+ made the walks time out. */
|
||||
export const HEAVY = [
|
||||
'tests/console-fill.test.ts', 'tests/site-overflow.test.ts', 'tests/local-fonts.test.ts', 'tests/glyphs.test.ts',
|
||||
'tests/capture-web.test.ts', 'tests/console-session.test.ts', 'tests/console-persist.test.ts', 'tests/console-flow.test.ts',
|
||||
'tests/glyph-ligatures.test.ts', 'tests/feature-fit.test.ts',
|
||||
'tests/browser/console-fill.browser.ts', 'tests/browser/site-overflow.browser.ts', 'tests/browser/local-fonts.browser.ts', 'tests/browser/glyphs.browser.ts',
|
||||
'tests/browser/capture-web.browser.ts', 'tests/browser/console-session.browser.ts', 'tests/browser/console-persist.browser.ts', 'tests/browser/console-flow.browser.ts',
|
||||
'tests/browser/glyph-ligatures.browser.ts', 'tests/browser/feature-fit.browser.ts',
|
||||
];
|
||||
|
||||
/** Browser tests of one file at the same time (bun test --max-concurrency for the browser shards; the tests opt in
|
||||
|
|
@ -31,7 +31,7 @@ export function browserShardCount(env = process.env): number {
|
|||
/**
|
||||
* The local pre-push gate on a busy shared machine: with the 1-minute load average above 2x the cores, the browser
|
||||
* suites (HEAVY) and the desktop boot check (real Chrome) are deferred to the Forgejo CI on the VDS, which always runs
|
||||
* them (CI=full; a red Forgejo CI on main stops releases). CI=full forces them locally.
|
||||
* them. Kept for the record: CI has no browser suites any more (tests/browser/, `bun run test:browser`).
|
||||
*/
|
||||
export function deferBrowser(env = process.env, load1 = loadavg()[0] ?? 0, cores = cpus().length): boolean {
|
||||
return env.CI !== 'full' && load1 > 2 * cores;
|
||||
|
|
|
|||
25
tools/ci.ts
25
tools/ci.ts
|
|
@ -15,14 +15,14 @@
|
|||
// no-hand-written-SVG guard; the desktop boot check (tools/desktop-boot-check.ts: the built window in headless
|
||||
// Chromium, needs playwright's browser or Google Chrome) runs after the desktop build.
|
||||
// The desktop Rust host (cargo test, clippy with warnings as errors) runs when cargo is installed and
|
||||
// apps/desktop/src-tauri or the sidecar build changed against origin/main, or always with CI=full;
|
||||
// apps/desktop/src-tauri or the sidecar build changed against origin/main (STOREBOUND_CI_BASE);
|
||||
// STOREBOUND_CI_RUST=0 skips it on a machine without Rust.
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
||||
import { cpus, homedir, loadavg, tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { hostTarget, OUT_DIR, outName } from './build-sidecars';
|
||||
import { loadFactor, loadNote, RETRY_MARK, scaled } from '../tests/helpers/load';
|
||||
import { BROWSER_CONCURRENCY, changedFiles, deferBrowser, HEAVY, junitTimes, listTests, mergeTimings, planShards, readTimings, relatedTests, ROOT, rustNeeded, writeTimings } from './ci-plan';
|
||||
import { BROWSER_CONCURRENCY, changedFiles, HEAVY, junitTimes, listTests, mergeTimings, planShards, readTimings, relatedTests, ROOT, rustNeeded, writeTimings } from './ci-plan';
|
||||
|
||||
interface Task { name: string; cmd: string[]; cwd?: string; env?: Record<string, string>; after?: string[]; junit?: string }
|
||||
|
||||
|
|
@ -30,10 +30,9 @@ const quick = process.argv.includes('--quick');
|
|||
const tauri = join(ROOT, 'apps/desktop/src-tauri');
|
||||
const cache = join(ROOT, 'node_modules/.cache/storebound-ci');
|
||||
const changed = changedFiles();
|
||||
const deferred = deferBrowser();
|
||||
const allTests = listTests();
|
||||
const tests = deferred ? allTests.filter(f => !HEAVY.includes(f)) : allTests;
|
||||
if (deferred) console.log(`ci: browser suites deferred to Forgejo CI (load ${loadavg()[0]!.toFixed(0)} > 2 x ${cpus().length} cores): ${allTests.filter(f => HEAVY.includes(f)).join(' ')} and the desktop boot check. CI=full runs them here.`);
|
||||
// no browser runs in CI: the browser suites live in tests/browser/*.browser.ts and run only with `bun run test:browser`;
|
||||
// the one browser step left is the desktop boot check inside `tools/desktop-release.ts mac` (release time)
|
||||
const tests = listTests();
|
||||
const touches = (re: RegExp) => changed === null || changed.some(f => re.test(f));
|
||||
const tasks: Task[] = [];
|
||||
const add = (t: Task) => tasks.push(t);
|
||||
|
|
@ -100,11 +99,8 @@ if (quick) {
|
|||
add({ name: 'desktop build', cmd: ['bunx', 'vite', 'build', '--logLevel', 'warn'], cwd: join(ROOT, 'apps/desktop') });
|
||||
// the release bundle must not carry the dev fixture mode
|
||||
add({ name: 'desktop bundle check', cmd: ['bun', 'test', './apps/desktop/tests/webview.test.ts'], after: ['desktop build'] });
|
||||
// the built window boots (release CSP, Tauri's frozen Object.prototype, mocked host + real sidecar) on an empty, a
|
||||
// D1 (folders gone) and a full D3 saved state, through every screen: no uncaught error, never an empty window
|
||||
if (!deferred) add({ name: 'desktop boot check', cmd: ['bun', 'tools/desktop-boot-check.ts'], after: ['desktop build'] });
|
||||
// the CLI compiled into the desktop sidecar (bun build --compile, scanned for this machine's paths): rebuilt when
|
||||
// its inputs changed, with CI=full, or when missing; tauri's build script needs it for cargo test / clippy
|
||||
// its inputs changed or when missing; tauri's build script needs it for cargo test / clippy
|
||||
const hostBin = join(OUT_DIR, outName(hostTarget()));
|
||||
const sidecarBuild = process.env.CI === 'full' || !existsSync(hostBin) || touches(/^(cli\/|tools\/build-sidecars\.ts$|package\.json$|bun\.lock$)/);
|
||||
if (sidecarBuild) add({ name: 'desktop sidecar (host)', cmd: ['bun', 'tools/build-sidecars.ts', '--host'] });
|
||||
|
|
@ -116,14 +112,6 @@ if (quick) {
|
|||
}
|
||||
}
|
||||
|
||||
// The browser suites fall back to Google Chrome without playwright's own headless Chromium; Chrome's shutdown takes
|
||||
// seconds to half a minute per browser, which makes those shards several times slower.
|
||||
if (tasks.some(t => t.name === 'desktop boot check' || t.cmd.some(a => HEAVY.some(h => a.endsWith(h))))) try {
|
||||
const { registry } = await import('playwright-core/lib/server/registry/index' as string);
|
||||
const exe = registry.findExecutable('chromium-headless-shell')?.executablePath('javascript') as string | undefined;
|
||||
if (exe && !existsSync(exe)) console.log(`ci: playwright's headless Chromium is not installed (${exe.replace(homedir(), '~')}): the browser tests use Google Chrome, which is much slower to close. Install it once: bunx playwright install chromium-headless-shell`);
|
||||
} catch { /* playwright internals moved: no hint */ }
|
||||
|
||||
// ---- runner ----
|
||||
|
||||
interface Run { task: Task; start: number; end?: number; code?: number; proc?: ReturnType<typeof Bun.spawn> }
|
||||
|
|
@ -199,6 +187,5 @@ for (const r of [...runs.values()].sort((a, b) => a.start - b.start)) {
|
|||
}
|
||||
for (const t of pending) console.log(` ${t.name.padEnd(width)} - not started`);
|
||||
if (retried.length) console.log(`\nretried under load (${retried.length}; each timed out once while load ÷ cores was above 2, then ran again):\n${retried.map(r => ` ${r}`).join('\n')}`);
|
||||
if (deferred) console.log(`\nci: browser suites deferred to Forgejo CI (load ${loadavg()[0]!.toFixed(0)}); check its run on main`);
|
||||
if (failed) { console.error(`\nci: ${failed} failed`); process.exit(1); }
|
||||
console.log(`\n${quick ? 'ci:quick' : 'ci'}: all green`);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue