@parity/product-lab 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +27 -0
- package/adapters/browser/playwright/README.md +2 -0
- package/adapters/browser/playwright/src/page-frame-capture.js +10 -2
- package/adapters/chain/papi/README.md +3 -1
- package/adapters/environment/engine/README.md +22 -4
- package/adapters/environment/engine/src/engine/engine-config.d.ts +1 -0
- package/adapters/environment/engine/src/engine/fork-bundle.d.ts +1 -1
- package/adapters/environment/engine/src/engine/fork-bundle.js +2 -2
- package/adapters/environment/engine/src/engine/fork-refresh.d.ts +4 -7
- package/adapters/environment/engine/src/engine/fork-refresh.js +52 -23
- package/adapters/environment/engine/src/engine/published-bundle.d.ts +19 -15
- package/adapters/environment/engine/src/engine/published-bundle.js +44 -51
- package/adapters/environment/engine/src/saved-fork/saved-fork-tooling.d.ts +1 -1
- package/adapters/environment/engine/src/saved-fork/saved-fork-tooling.js +5 -2
- package/adapters/environment/engine/src/saved-fork/saved-fork.js +1 -1
- package/adapters/environment/engine/src/setup/engine-build.d.ts +4 -4
- package/adapters/environment/engine/src/setup/engine-build.js +14 -4
- package/adapters/environment/engine/src/setup/patch-catalogue.d.ts +6 -0
- package/adapters/environment/engine/src/setup/patch-catalogue.js +5 -0
- package/adapters/environment/engine/src/setup/prepare-engine.d.ts +6 -5
- package/adapters/environment/engine/src/setup/prepare-engine.js +27 -10
- package/adapters/features/cucumber/README.md +4 -0
- package/adapters/features/cucumber/src/steps/register-feature-steps.js +15 -1
- package/adapters/system/node/README.md +3 -3
- package/adapters/video/ffmpeg/README.md +4 -0
- package/adapters/video/ffmpeg/src/ffmpeg-video-editor.d.ts +1 -1
- package/adapters/video/ffmpeg/src/ffmpeg-video-editor.js +19 -6
- package/core/application/README.md +30 -1
- package/core/application/src/features/direct-feature-steps.d.ts +23 -0
- package/core/application/src/features/direct-feature-steps.js +25 -0
- package/core/application/src/features/feature-recording.d.ts +6 -0
- package/core/application/src/features/feature-recording.js +3 -0
- package/core/application/src/features/feature-scenario.d.ts +10 -8
- package/core/application/src/features/feature-scenario.js +11 -15
- package/core/application/src/features/feature-step-times.d.ts +11 -1
- package/core/application/src/features/feature-step-times.js +20 -6
- package/core/application/src/features/lab-feature-session.d.ts +3 -1
- package/core/application/src/features/lab-feature-session.js +5 -2
- package/core/application/src/features/run-feature-steps.d.ts +3 -1
- package/core/application/src/features/run-feature-steps.js +4 -2
- package/core/application/src/lab-runtime-use-cases.js +5 -1
- package/core/application/src/recordings/demo-markup.d.ts +8 -0
- package/core/application/src/recordings/demo-markup.js +11 -1
- package/core/application/src/recordings/demo-recording.d.ts +0 -2
- package/core/application/src/recordings/demo-recording.js +40 -23
- package/core/application/src/recordings/frame-sources.d.ts +20 -1
- package/core/application/src/recordings/frame-sources.js +89 -0
- package/core/application/src/recordings/record-runs.d.ts +7 -1
- package/core/application/src/recordings/record-runs.js +66 -11
- package/core/application/src/recordings/recording-draft.d.ts +4 -0
- package/core/application/src/recordings/recording-draft.js +13 -4
- package/core/domain/README.md +14 -0
- package/core/domain/src/recordings/demo-cut.js +6 -4
- package/core/domain/src/recordings/demo-holds.d.ts +12 -0
- package/core/domain/src/recordings/demo-holds.js +31 -0
- package/core/domain/src/recordings/index.d.ts +1 -0
- package/core/domain/src/recordings/index.js +1 -0
- package/core/domain/src/recordings/recording-data.d.ts +9 -1
- package/core/domain/src/recordings/recording-settings.d.ts +5 -1
- package/core/domain/src/recordings/recording-settings.js +7 -0
- package/core/domain/src/recordings/run-recording.d.ts +2 -0
- package/core/domain/src/recordings/run-recording.js +8 -1
- package/core/ports/README.md +6 -0
- package/core/ports/src/driven/recording/demo-ports.d.ts +2 -0
- package/core/ports/src/driven/recording/frame-capture.d.ts +4 -0
- package/core/ports/src/driving/demo.d.ts +2 -0
- package/core/ports/src/driving/feature-session.d.ts +7 -0
- package/entrypoints/cli/README.md +6 -3
- package/entrypoints/cli/src/demo-command.d.ts +1 -1
- package/entrypoints/cli/src/demo-command.js +10 -5
- package/entrypoints/mcp/README.md +3 -0
- package/entrypoints/panel/README.md +6 -3
- package/entrypoints/panel/src/visible/detached-identities.js +5 -1
- package/entrypoints/panel/src/visible/window-frames.js +14 -1
- package/package.json +1 -1
|
@@ -179,7 +179,7 @@ environment as unable to start when its sessions' default host runs here on a li
|
|
|
179
179
|
| `AdvanceEnvironmentTime` | `src/environment/advance-environment-time.ts`; the lab wrapper's check in `src/orchestration/lab/checked-environment-time.ts` | New (backlog 16, 30 September 2026): runs the generic time sentences (`the chain advances {int} blocks`, `time advances by {int} minutes`, kind `time`) through the running environment's `time` control, over every chain it lists, as setup work (a run the setup principal owns, credited to no participant); returns each chain's advance (`fromBlock`, `toBlock`, the clock at both, `checkedBy`) as the step's evidence. `unavailable` gives the environment's own reason (no control; a time sentence where it only builds blocks), which the feature session reports before the scenario runs; the plan also refuses a value outside `TimeAdvance`'s limits and an advance before the first actor. The lab wrapper reads each chain's finalized head and `Timestamp.Now` through its own connection before and after, and fails the step when either the environment's report or its own reading falls short (`tests/time-advance.test.ts`, with the negative control) |
|
|
180
180
|
| `CheckFixtures`, `RefreshFixture`, `FixtureMaintenanceService` | `src/fixtures/check-fixtures.ts`, `refresh-fixture.ts`, `fixture-maintenance-service.ts` | New (keeping fixtures current, 30 September 2026, backlog 1i, same record): `LabRuntime.fixtureMaintenance()`. **Check** starts nothing: it compares each declared fixture's saved key with this checkout's setup, lock and inputs. **Plan**, with the fixture's environment running, splits the key's differences into checkout parts, state (environment, lineage) and runtimes. When the runtimes alone differ and `EnvironmentFixtures.upgradable()` allows it, the plan is `upgrade`; otherwise it is `rebuild`, naming why. **Upgrade** needs the `restoreFixture` confirmation. With the session closed it restores the saved state as it is, then calls `EnvironmentSnapshots.upgrade`. It then saves again through `SaveFixture` with an `upgraded` history entry, keeping the earlier history (`SaveFixture.execute({ history })`). **Rebuild** runs in two steps. `prepareRebuild` needs no environment running: it sets the saved session directories aside (`FixtureStore.setAside`). After the caller has run the setup, `saveRebuilt` records `rebuilt` with the reasons and what the fixture replaced. Tests: `tests/fixture-maintenance.test.ts` |
|
|
181
181
|
| `RunSuite` | `src/suites/run-suite.ts` | New (Phase 7): `product-lab test --suite`. Plans the suite in the domain from the lab's `suites`, features, hosts, version policy and changes (`ChangeSource` for a base, or given paths); runs each runnable row as a feature run (`RunFeatures`: tags, environment, `PRODUCT_LAB_HOST`, candidates resolved once into a run manifest, the lock untouched), one after the other, stopping the row the budget runs out in (a stop that fails, on the budget or an interrupt, is named in the row's reason and fails a row that passed); judges with `judgeSuite`. Modes (`defineLab({ modes })`, `product-lab test --mode`, `SuiteRequest.mode`) plan and run here as suites of their own name: a row's device goes to its run as `PRODUCT_LAB_DEVICE` (checked against the browser's named devices), a row shared out among workers gets exactly its files, a mode's `workers` rows run in waves started together (`RunFeatures.executeTogether`, each with the lab's `workers.env` for its slot; refused without a declaration or above its `max`), and an old environment id names its environment through the lab's aliases (`tests/modes.test.ts`). A row plans exactly what its run runs: the feature runner's `selection` (its configuration loaded with the row's host) gives the files, unless the lab declares its own features (which the row's run then gets as its paths), and its tags and fail-fast always. After the run every planned scenario it did not report is named on the row (`unreported`, `unreportedScenarios`), with why when known (stopped by the budget or an interrupt; after the first failure under fail-fast); one without a reason fails a run that passed. A row that resolved a `host:<kind>` release from a version source asks its host adapter how to run it (`routeRelease`, backlog 27, `row-host-route.ts`): an unsupported route makes the row `unsupported` (never run, never passing, with the reason); otherwise the route goes to the run in `PRODUCT_LAB_HOST_ROUTE`, and the row records `hostRoute` (with the patch set), `timings.hostRouteMs` and how its host ran in the combination. Each record carries `counts` (`suite-metrics.ts`: scenarios planned, reported, executed, passed, failed, skipped, unsupported, not reported; checkpoints) and measured `timings` (version resolution, the run, each environment start the row's run recorded through `RunTimingsFile` in `PRODUCT_LAB_RUN_TIMINGS`, the reported step time, the row total; the suite's planning, critical path and total). Nothing is estimated: an absent value was not measured. `tests/suites.test.ts` |
|
|
182
|
-
| `RecordRuns` | `src/recordings/record-runs.ts` | New (Phase 9): follows `run-started`, `run-finished` and the environment events, and each run's step and status events; one recording per configured scenario run and one per feature scenario (its `feature:<scenario>#<n>` runs grouped under the in-process feature run, else while they keep coming); since 3 October 2026 (backlog 20) a feature scenario's recording names where it was written (`feature`: uri, line, tags, fixture) from what its runner gave `FeatureSession.begin` (`LabState.scenarioSources`), so a Cucumber process's recordings carry it too, and an in-process run's report replaces it; how the runner saw the scenario end (`FeatureScenarioHandle.end(outcome)`, `LabState.scenarioOutcomes`) makes a scenario that failed outside the lab's runs (a custom step, a hook) a failed recording with `runnerFailure`, never a passed one. Asks for a frame of the acting identity as a step starts and ends and of every identity at a checkpoint (a scenario run's pause; since 30 September 2026 also an in-process feature run's `feature-paused`, where every identity the scenario used and every running identity is taken, so a stepping feature's next "before" frame is settled: `tests/recording-feature-pause.test.ts`),
|
|
182
|
+
| `RecordRuns` | `src/recordings/record-runs.ts` | New (Phase 9): follows `run-started`, `run-finished` and the environment events, and each run's step and status events; one recording per configured scenario run and one per feature scenario (its `feature:<scenario>#<n>` runs grouped under the in-process feature run, else while they keep coming); since 3 October 2026 (backlog 20) a feature scenario's recording names where it was written (`feature`: uri, line, tags, fixture) from what its runner gave `FeatureSession.begin` (`LabState.scenarioSources`), so a Cucumber process's recordings carry it too, and an in-process run's report replaces it; how the runner saw the scenario end (`FeatureScenarioHandle.end(outcome)`, `LabState.scenarioOutcomes`) makes a scenario that failed outside the lab's runs (a custom step, a hook) a failed recording with `runnerFailure`, never a passed one. Asks for a frame of the acting identity as a step starts and ends and of every identity at a checkpoint (a scenario run's pause; since 30 September 2026 also an in-process feature run's `feature-paused`, where every identity the scenario used and every running identity is taken, so a stepping feature's next "before" frame is settled: `tests/recording-feature-pause.test.ts`), without waiting in default frames-only runs; video feature boundaries await bounded after-state captures and keep a 2-second tail before cleanup (each frame records its timing and the steps that overlapped it; no source, a throw or a timeout is `unavailable` or `failed` with the reason); records the chain heads observed at each step start and end, transactions and evidence blocks; redacts everything with the runtime's `Redactor` and writes through the `RecordingStore` with retention (`recording-saved`, `recording-failed` events). Helpers `RecordingDraft`, `RecordingStores`, `FrameSources` (the lab window's registered source first, else `LabEnvironment.frames`). Since backlog 18b (headless half; 52) a step's start also asks the source for its identity's page video (`FrameCapture.video`), named once per file in `capture.videos` with when its page opened, with a note that videos are not redacted; a video that cannot be named is a note, never a guess. `tests/recordings.test.ts` |. Since backlog 20 (3 October 2026) a feature recording also names its scenario (`feature`: file, line, tags, fixture), from the feature run's report when the run completes Since backlog 110 (5 October 2026) it keeps what each step did on screen (`uiCalls`): the frame source's own UI calls (`FrameCapture.uiCalls`, the lab window's) read as a step starts (what came between steps is left out) and as it ends, with a cursor per source; a source that keeps none gives no block (`tests/recordings.test.ts`). Since backlog 110 a feature scenario's recording also keeps its Gherkin steps with their times (`featureSteps`, kept by `FeatureScenario` through `timedFeatureStep` in `src/features/feature-step-times.ts` in `LabState.scenarioSteps`), the identity steps that run no lab run included, and starts where its first step did (`tests/recording-feature-steps.test.ts`).
|
|
183
183
|
| `BundleRecording`, `OpenRunBundle`, `ReproduceRun`, `controlSteps`, `RunBundlesService`, `RunBundleSources` | `src/runs/bundles/` | New (backlog 20, 3 October 2026): a recorded feature scenario written as one run bundle (the recording verbatim, the feature file, the lab lock, the run manifest, the fixture it started from as fixture export writes it, the lab's and each app's source revision through `ChangeSource.revision`); an identity whose key comes from a lab secret (`LabCatalog.preOnboardedActors`) is withheld and named, and a plain-text entry holding a lab secret's value refuses the bundle. `OpenRunBundle` reads the index first, extracts and checks every entry, shows each recording read-only (`recordingView`) and names every reason a reproduction here would be refused (`RunBundleReproduction`). `ReproduceRun` refuses with every reason first, then rebuilds the starting state (fixture imported; else the environment started, or the running one reset with its confirmation), reruns the scenario in process, judges the rerun's recording (`controlSteps`) against the bundled failure and writes a bundle holding every run; with `reruns` (backlog 26) it reruns from the same rebuilt state up to that many times, stopping at the first rerun that does not fail (verdict `flaky`, never passing). Ports: `RunBundlePorts` (`archives`, `recordingsAt`). Tests: `composition/tests/run-bundles.test.ts`, `tests/reproduce-run.test.ts` | Since backlog 110 `BundleRecording.reproducibility(id)` (`LabRunBundles.reproducibility`) says before anything is bundled or started whether a recording's run can be reproduced, from the facts a bundle records (feature file, public network, starting state, fixture, keys from lab secrets), in plain words with one way forward (`tests/reproducibility.test.ts`).
|
|
184
184
|
| `BisectRegression`, `ProbeSession` | `src/bisect/bisect-regression.ts`, `src/bisect/probe-session.ts` | New (backlog 37, 4 October 2026): `product-lab bisect`. Reads two suite records, compares them as `report compare` does and takes one change; the domain's `bisectPlan` gives its axes or refuses it. An app axis is the lab's commits (`ChangeSource.commits`), a runtime or host axis the published releases between the two versions (`VersionSource.publishedVersions`). Each probe runs the one scenario as its row ran it (`RunSuite.probe`, every version pinned), twice by default, and the domain judges it; the ends first. With several axes each is first moved alone to its bad value. A probe of the lab's source runs from a worktree under the lab's cache with its dependencies installed frozen from its own lockfile (the package cache under the lab's cache; which lockfile and whether the cache was warm are recorded), and every worktree is removed in a `finally` block. Nothing starts below 15 GB of free disk (`BISECT_MIN_FREE_DISK`); each probe's disk use is recorded. Writes `product-lab-bisect.json` and `summary.md`; `reproduce` is one shell command that reruns the first bad value (for the lab's source, from the repository's top: `git worktree add`, the frozen install, the run from the lab's directory). A probe's worktree report is read through `readReport` (the feature runner's reader of its Cucumber JSON). Test: `tests/bisect-regression.test.ts` |
|
|
185
185
|
| `ListRecordings`, `OpenRecording` | `src/recordings/` | New (Phase 9): the history and one recording with the domain's frame choice per step and identity, and a frame's image. Read the store only: no environment needed, nothing changes (`tests/recordings.test.ts` checks the runtime is unchanged, with a negative control) | Since backlog 25 `RecordingView.health` gives each step's health and findings (`RecordingHealth.DEFAULT`), so the lab window shows what `product-lab recordings` shows.
|
|
@@ -515,3 +515,32 @@ A step aborted by its limit, cancellation or interruption keeps the abort reason
|
|
|
515
515
|
pass through the configured error describer. `StepReport.reason` and scheduling decisions
|
|
516
516
|
still use the abort reason and original error text; reconciliation receives that original reason, and quarantine
|
|
517
517
|
keeps it. See `tests/scheduler-abort-diagnostics.test.ts`.
|
|
518
|
+
|
|
519
|
+
## Held feature demos
|
|
520
|
+
|
|
521
|
+
FeatureScenario observes direct definitions through startStep/finishStep as well as generated runs. RecordRuns opens at the first feature step. Frames-only capture remains asynchronous; video runs await bounded captures at step boundaries and keep a 2 s tail before closing scenario services. Frames name their 1-based featureStep, including steps with no tool run. DemoRecording prefers these frames, uses planDemoHolds, renders a separate caption band, and decodes step-001.png and later midpoint images from the encoded cut. Slide-only output without ffmpeg uses the last available recorded frame with an explicit label when it predates the final step. Tests cover coverage, reload tail, direct failures and the former planner as a negative control.
|
|
522
|
+
|
|
523
|
+
Scenario cleanup settles pending captures before finalizing its recording, then awaits its host-log read before
|
|
524
|
+
closing identities and scenario services. Recording writes can still await background transaction checks; cleanup
|
|
525
|
+
does not wait for those writes.
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
Video feature after-frames first check that the current document has completed loading, then compare exact PNGs
|
|
529
|
+
across a 200 ms window, sampling at least 100 ms apart. The stability wait is capped at 3000 ms by default
|
|
530
|
+
(`RecordingSettings.settleTimeoutMs`, or `PRODUCT_LAB_RECORDINGS_SETTLE_MS`). At the cap, the recorder takes a
|
|
531
|
+
fresh frame anyway and adds `settled: false` and `waitedMs` to its metadata. Successful checks add `settled: true`;
|
|
532
|
+
`loadComplete` records when the source verified the document. The existing `captureTimeoutMs` still bounds the
|
|
533
|
+
whole capture, including this wait and any final image. The wait never changes the step's own timestamps or outcome.
|
|
534
|
+
|
|
535
|
+
Only feature after-frames with video enabled wait for visual stability, including the final frame after the
|
|
536
|
+
2-second scenario tail. Tool-run captures, before-frames, checkpoints and default frames-only feature captures
|
|
537
|
+
retain their existing asynchronous policy. Scenario cleanup still settles captures already in flight. Custom
|
|
538
|
+
capture sources may ignore the optional load check; absent `loadComplete` means load completion is unknown.
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
The effective stability cap reserves time for the final screenshot: it is clamped to `captureTimeoutMs` minus
|
|
542
|
+
1000 ms, or minus half that timeout when it is shorter than 2000 ms. With the default 5000 ms capture timeout,
|
|
543
|
+
`PRODUCT_LAB_RECORDINGS_SETTLE_MS` accepts positive values but uses at most 4000 ms; the default stays 3000 ms.
|
|
544
|
+
A captured frame with `settled: false` is labelled "Taken at the stability cap" in recording-view notes and in
|
|
545
|
+
the demo image manifest and captions, including reused setup and failure context. Older frames without this
|
|
546
|
+
metadata retain their existing labels.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { LabState } from "../lab/lab-state.js";
|
|
2
|
+
import type { FeatureRecording } from "./feature-recording.js";
|
|
3
|
+
import type { ScenarioPlan } from "@parity/product-lab/domain";
|
|
4
|
+
/** Observes custom definitions without treating them as scheduler-owned work. */
|
|
5
|
+
export declare class DirectFeatureSteps {
|
|
6
|
+
private readonly options;
|
|
7
|
+
constructor(options: {
|
|
8
|
+
state: LabState;
|
|
9
|
+
scenario: string;
|
|
10
|
+
now: () => number;
|
|
11
|
+
redact: (text: string) => string;
|
|
12
|
+
plan: ScenarioPlan;
|
|
13
|
+
ended: () => boolean;
|
|
14
|
+
ran: Set<number>;
|
|
15
|
+
identities: () => string[];
|
|
16
|
+
recording?: FeatureRecording;
|
|
17
|
+
});
|
|
18
|
+
start(index: number): void;
|
|
19
|
+
finish(index: number, outcome: {
|
|
20
|
+
status: "passed" | "failed";
|
|
21
|
+
error?: string;
|
|
22
|
+
}): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { startFeatureStep } from "./feature-step-times.js";
|
|
4
|
+
export class DirectFeatureSteps {
|
|
5
|
+
options;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
this.options = options;
|
|
8
|
+
}
|
|
9
|
+
start(index) {
|
|
10
|
+
const step = this.options.plan.steps[index];
|
|
11
|
+
if (!step?.custom || this.options.ended() || this.options.ran.has(index))
|
|
12
|
+
return;
|
|
13
|
+
this.options.ran.add(index);
|
|
14
|
+
const at = this.options.now();
|
|
15
|
+
startFeatureStep(this.options.state, this.options.scenario, index, step.text, at);
|
|
16
|
+
this.options.recording?.stepStarted(this.options.scenario, at);
|
|
17
|
+
}
|
|
18
|
+
async finish(index, outcome) {
|
|
19
|
+
const step = this.options.state.scenarioSteps.get(this.options.scenario)?.find((item) => item.featureStep === index + 1);
|
|
20
|
+
if (!step || step.finishedAt !== undefined)
|
|
21
|
+
return;
|
|
22
|
+
Object.assign(step, { status: outcome.status, finishedAt: this.options.now(), ...(outcome.error ? { error: this.options.redact(outcome.error) } : {}) });
|
|
23
|
+
await this.options.recording?.stepFinished(this.options.scenario, index + 1, this.options.identities());
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Recording boundaries include direct/custom feature steps that create no runtime run. */
|
|
2
|
+
export interface FeatureRecording {
|
|
3
|
+
stepStarted(scenario: string, at: number): void;
|
|
4
|
+
stepFinished(scenario: string, featureStep: number, identities: readonly string[]): Promise<void>;
|
|
5
|
+
scenarioEnded(scenario: string): Promise<void>;
|
|
6
|
+
}
|
|
@@ -8,7 +8,9 @@ import type { ManageActors } from "../actors/manage-actors.js";
|
|
|
8
8
|
import type { LabState } from "../lab/lab-state.js";
|
|
9
9
|
import type { RunScenario } from "../runs/run-scenario.js";
|
|
10
10
|
import type { ScenarioStepChecks } from "./scenario-step-checks.js";
|
|
11
|
+
import type { FeatureRecording } from "./feature-recording.js";
|
|
11
12
|
export interface ScenarioDependencies {
|
|
13
|
+
recording?: FeatureRecording;
|
|
12
14
|
id: string;
|
|
13
15
|
state: LabState;
|
|
14
16
|
runScenario: RunScenario;
|
|
@@ -61,6 +63,7 @@ export declare class FeatureScenario implements FeatureScenarioHandle {
|
|
|
61
63
|
private readonly ownedRuns;
|
|
62
64
|
/** New actors created ahead of their steps (scenario-actor-creation.ts). */
|
|
63
65
|
private readonly creation;
|
|
66
|
+
private readonly directSteps;
|
|
64
67
|
/** Backlog 31: the controls this scenario set a condition through, by identity: each is reset when it ends. */
|
|
65
68
|
private readonly conditioned;
|
|
66
69
|
readonly outputs: StepOutputs;
|
|
@@ -78,22 +81,21 @@ export declare class FeatureScenario implements FeatureScenarioHandle {
|
|
|
78
81
|
/** What a paused run shows about this scenario (backlog 17; scenario-inspection.ts). */
|
|
79
82
|
inspect(next: number): ScenarioInspection;
|
|
80
83
|
/** Every output recorded so far, the ones the scenario began with included (backlog 21). */
|
|
81
|
-
recorded()
|
|
82
|
-
trace()
|
|
84
|
+
recorded: () => readonly RecordedOutput[];
|
|
85
|
+
trace: () => readonly FeatureStepTrace[];
|
|
83
86
|
run(index: number): Promise<FeatureStepTrace>;
|
|
84
87
|
private runStep;
|
|
88
|
+
startStep: (index: number) => void;
|
|
89
|
+
finishStep: (index: number, outcome: {
|
|
90
|
+
status: "passed" | "failed";
|
|
91
|
+
error?: string;
|
|
92
|
+
}) => Promise<void>;
|
|
85
93
|
private stepBody;
|
|
86
94
|
/**
|
|
87
95
|
* Backlog 40: after a step passed, each actor that acted in it is checked (a screenshot against its baseline, an
|
|
88
96
|
* accessibility scan); what the checks found is kept in the step's trace, and a failure fails the step.
|
|
89
97
|
*/
|
|
90
98
|
private checkStep;
|
|
91
|
-
/**
|
|
92
|
-
* A run that shows each actor's app on a surface of its own (the lab window's tiles): an identity call ends once each
|
|
93
|
-
* of its actors' surfaces shows the app ready, so no later step acts on, or conditions, an app that has not loaded.
|
|
94
|
-
* The session readied its own page; a tile is another document of the same app.
|
|
95
|
-
*/
|
|
96
|
-
private surfacesReady;
|
|
97
99
|
/**
|
|
98
100
|
* `{actor} is a new actor` / `these actors exist:`: create an identity per new name (manual mode reuses a bound
|
|
99
101
|
* one). When the environment creates several identities at once (`actorCreation.concurrency`), the new names of
|
|
@@ -14,8 +14,9 @@ import { RunScopedValues } from "./run-scoped-values.js";
|
|
|
14
14
|
import { ConditionProfiles, ConditionStep, describeNetwork } from "@parity/product-lab/domain/conditions";
|
|
15
15
|
import { conditionControl } from "./scenario-conditions.js";
|
|
16
16
|
import { raceStepDelay } from "./scenario-race.js";
|
|
17
|
-
import { readIdentities, timedFeatureStep } from "./feature-step-times.js";
|
|
17
|
+
import { readIdentities, readyFeatureSurfaces, timedFeatureStep } from "./feature-step-times.js";
|
|
18
18
|
import { ScenarioActorCreation } from "./scenario-actor-creation.js";
|
|
19
|
+
import { DirectFeatureSteps } from "./direct-feature-steps.js";
|
|
19
20
|
const NAVIGATION_WAIT_MS = 30_000;
|
|
20
21
|
export class FeatureScenario {
|
|
21
22
|
name;
|
|
@@ -38,6 +39,7 @@ export class FeatureScenario {
|
|
|
38
39
|
inFlight = new Set();
|
|
39
40
|
ownedRuns = new Set();
|
|
40
41
|
creation;
|
|
42
|
+
directSteps;
|
|
41
43
|
conditioned = new Map();
|
|
42
44
|
outputs;
|
|
43
45
|
fixture;
|
|
@@ -59,6 +61,7 @@ export class FeatureScenario {
|
|
|
59
61
|
this.outputs = this.view(undefined);
|
|
60
62
|
this.creation = new ScenarioActorCreation({ steps: plan.steps, manageActors: deps.manageActors, ...(deps.clock ? { clock: deps.clock } : {}), signal: this.abort.signal, environment: () => this.active.environment, bound: (name) => this.bindings.has(name), conditions: () => this.conditionedNames(), created: (id) => { if (this.binding === "fresh")
|
|
61
63
|
this.created.push(id); }, chosenHost: (name) => deps.hosts?.[name], scenario: this.name });
|
|
64
|
+
this.directSteps = new DirectFeatureSteps({ state: deps.state, scenario: name, now: () => deps.runScenario.clock.date().getTime(), redact: (text) => deps.redactor.text(text), identities: () => this.identities(), ...(deps.recording ? { recording: deps.recording } : {}), plan, ended: () => this.ended, ran: this.ran });
|
|
62
65
|
}
|
|
63
66
|
current() {
|
|
64
67
|
const active = this.deps.state.active;
|
|
@@ -90,12 +93,8 @@ export class FeatureScenario {
|
|
|
90
93
|
inspect(next) {
|
|
91
94
|
return inspectScenario(this.plan, next, this.store, this.bindings, this.deps.redactor);
|
|
92
95
|
}
|
|
93
|
-
recorded()
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
trace() {
|
|
97
|
-
return this.deps.redactor.value(this.steps);
|
|
98
|
-
}
|
|
96
|
+
recorded = () => this.store.entries();
|
|
97
|
+
trace = () => this.deps.redactor.value(this.steps);
|
|
99
98
|
run(index) {
|
|
100
99
|
if (this.ended)
|
|
101
100
|
return Promise.reject(new Error(`Scenario "${this.name}" has ended`));
|
|
@@ -117,8 +116,10 @@ export class FeatureScenario {
|
|
|
117
116
|
this.ran.add(index);
|
|
118
117
|
this.current();
|
|
119
118
|
const now = () => this.deps.runScenario.clock.date().getTime();
|
|
120
|
-
return timedFeatureStep(this.deps.state, this.name, index, step.text, now, (text) => this.deps.redactor.text(text), () => this.stepBody(index, step));
|
|
119
|
+
return timedFeatureStep(this.deps.state, this.name, index, step.text, now, (text) => this.deps.redactor.text(text), () => this.stepBody(index, step), { recording: this.deps.recording, identities: () => this.identities() });
|
|
121
120
|
}
|
|
121
|
+
startStep = (index) => this.directSteps.start(index);
|
|
122
|
+
finishStep = (index, outcome) => this.directSteps.finish(index, outcome);
|
|
122
123
|
async stepBody(index, step) {
|
|
123
124
|
const calls = step.calls.map((call) => ({
|
|
124
125
|
owner: call.entry.owner,
|
|
@@ -158,7 +159,7 @@ export class FeatureScenario {
|
|
|
158
159
|
await flush();
|
|
159
160
|
try {
|
|
160
161
|
await this.identity(call, calls[position], index);
|
|
161
|
-
await this.
|
|
162
|
+
await readyFeatureSurfaces(this.deps.surface, introducedNames(call), this.bindings, this.abort.signal);
|
|
162
163
|
calls[position].status = "complete";
|
|
163
164
|
}
|
|
164
165
|
catch (error) {
|
|
@@ -187,12 +188,6 @@ export class FeatureScenario {
|
|
|
187
188
|
if (failures.length)
|
|
188
189
|
throw FeatureStepError.of(`Step ${index + 1} ("${step.text}"): ${failures.join("; ")}`, this.deps.redactor.value(trace), reasons?.[0]);
|
|
189
190
|
}
|
|
190
|
-
async surfacesReady(names) {
|
|
191
|
-
if (!this.deps.surface)
|
|
192
|
-
return;
|
|
193
|
-
const ids = [...new Set(names.map((name) => this.bindings.get(name)).filter((id) => id !== undefined))];
|
|
194
|
-
await Promise.all(ids.map((id) => this.deps.surface(id)?.ready?.({ signal: this.abort.signal })));
|
|
195
|
-
}
|
|
196
191
|
async identity(call, trace, index) {
|
|
197
192
|
const names = introducedNames(call);
|
|
198
193
|
const unfunded = call.entry.owner === NEW_UNFUNDED_ACTOR_OWNER;
|
|
@@ -572,6 +567,7 @@ export class FeatureScenario {
|
|
|
572
567
|
run.interrupt(reason);
|
|
573
568
|
await Promise.allSettled([...this.inFlight]);
|
|
574
569
|
await this.creation.settled();
|
|
570
|
+
await this.deps.recording?.scenarioEnded(this.name);
|
|
575
571
|
await this.deps.state.beforeClose.settle();
|
|
576
572
|
const errors = [];
|
|
577
573
|
for (const [actorId, control] of this.conditioned) {
|
|
@@ -1,7 +1,17 @@
|
|
|
1
|
+
import type { RecordedFeatureStep } from "@parity/product-lab/domain/recordings";
|
|
1
2
|
import type { LabEnvironment } from "@parity/product-lab/ports/lab";
|
|
2
3
|
import type { LabState } from "../lab/lab-state.js";
|
|
4
|
+
import type { FeatureRecording } from "./feature-recording.js";
|
|
5
|
+
import type { FeatureSessionOptions } from "@parity/product-lab/ports/driving";
|
|
3
6
|
/** Run step `index` (`work`) of scenario `name`, keeping its times and outcome; `redact` cleans a failure's message. */
|
|
4
|
-
export declare function timedFeatureStep<T>(state: LabState, name: string, index: number, text: string, now: () => number, redact: (text: string) => string, work: () => Promise<T
|
|
7
|
+
export declare function timedFeatureStep<T>(state: LabState, name: string, index: number, text: string, now: () => number, redact: (text: string) => string, work: () => Promise<T>, observe?: {
|
|
8
|
+
recording?: FeatureRecording;
|
|
9
|
+
identities: () => string[];
|
|
10
|
+
}): Promise<T>;
|
|
11
|
+
/** Direct/custom definitions use the same timing data as steps run through the scheduler. */
|
|
12
|
+
export declare function startFeatureStep(state: LabState, name: string, index: number, text: string, at: number): RecordedFeatureStep;
|
|
13
|
+
/** A visible tile is a separate document from the session's own page; ready it before later steps use it. */
|
|
14
|
+
export declare function readyFeatureSurfaces(surface: FeatureSessionOptions["surface"], names: readonly string[], bindings: ReadonlyMap<string, string>, signal: AbortSignal): Promise<void>;
|
|
5
15
|
/**
|
|
6
16
|
* Backlog 160: what the environment reads of the identities a step made, on chain and on their screens, kept as the
|
|
7
17
|
* step's checks (`LabState.featureStepReadings`). A reading the environment cannot take fails the step: a new identity
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
// Copyright 2026 Parity Technologies (UK) Ltd.
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
-
export async function timedFeatureStep(state, name, index, text, now, redact, work) {
|
|
4
|
-
const kept =
|
|
5
|
-
|
|
6
|
-
if (!steps)
|
|
7
|
-
state.scenarioSteps.set(name, (steps = []));
|
|
8
|
-
steps.push(kept);
|
|
3
|
+
export async function timedFeatureStep(state, name, index, text, now, redact, work, observe) {
|
|
4
|
+
const kept = startFeatureStep(state, name, index, text, now());
|
|
5
|
+
observe?.recording?.stepStarted(name, kept.startedAt);
|
|
9
6
|
try {
|
|
10
7
|
const result = await work();
|
|
11
8
|
Object.assign(kept, { status: "passed", finishedAt: now() });
|
|
@@ -15,6 +12,23 @@ export async function timedFeatureStep(state, name, index, text, now, redact, wo
|
|
|
15
12
|
Object.assign(kept, { status: "failed", finishedAt: now(), error: redact(error instanceof Error ? error.message : String(error)) });
|
|
16
13
|
throw error;
|
|
17
14
|
}
|
|
15
|
+
finally {
|
|
16
|
+
await observe?.recording?.stepFinished(name, index + 1, observe.identities());
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function startFeatureStep(state, name, index, text, at) {
|
|
20
|
+
const kept = { featureStep: index + 1, text, status: "running", startedAt: at };
|
|
21
|
+
let steps = state.scenarioSteps.get(name);
|
|
22
|
+
if (!steps)
|
|
23
|
+
state.scenarioSteps.set(name, (steps = []));
|
|
24
|
+
steps.push(kept);
|
|
25
|
+
return kept;
|
|
26
|
+
}
|
|
27
|
+
export async function readyFeatureSurfaces(surface, names, bindings, signal) {
|
|
28
|
+
if (!surface)
|
|
29
|
+
return;
|
|
30
|
+
const ids = [...new Set(names.map((name) => bindings.get(name)).filter((id) => id !== undefined))];
|
|
31
|
+
await Promise.all(ids.map((id) => surface(id)?.ready?.({ signal })));
|
|
18
32
|
}
|
|
19
33
|
export async function readIdentities(at, ids) {
|
|
20
34
|
const read = at.environment.actorReadings;
|
|
@@ -10,6 +10,7 @@ import type { PrepareScenarioFixture } from "../fixtures/prepare-scenario-fixtur
|
|
|
10
10
|
import type { IsolateScenario } from "../fixtures/isolate-scenario.js";
|
|
11
11
|
import type { KeepRunStart } from "../fixtures/keep-run-start.js";
|
|
12
12
|
import type { StepChecks } from "./step-checks.js";
|
|
13
|
+
import type { FeatureRecording } from "./feature-recording.js";
|
|
13
14
|
export declare class LabFeatureSession implements FeatureSession {
|
|
14
15
|
private readonly state;
|
|
15
16
|
private readonly runScenario;
|
|
@@ -28,6 +29,7 @@ export declare class LabFeatureSession implements FeatureSession {
|
|
|
28
29
|
private readonly clock?;
|
|
29
30
|
/** Keeps the state a scenario without a fixture starts on, when the lab's setting asks (5 October 2026). */
|
|
30
31
|
private readonly runStart?;
|
|
32
|
+
private readonly recording?;
|
|
31
33
|
/** The latest scenario begun under each name (what a pause gate names). */
|
|
32
34
|
private readonly begun;
|
|
33
35
|
constructor(state: LabState, runScenario: RunScenario, manageActors: ManageActors, redactor: Redactor, ids: IdGenerator, options: FeatureSessionOptions, conditions?: ReadScenarioConditions | undefined,
|
|
@@ -40,7 +42,7 @@ export declare class LabFeatureSession implements FeatureSession {
|
|
|
40
42
|
/** Times of new actors created ahead of their steps (scenario-actor-creation.ts). */
|
|
41
43
|
clock?: Clock | undefined,
|
|
42
44
|
/** Keeps the state a scenario without a fixture starts on, when the lab's setting asks (5 October 2026). */
|
|
43
|
-
runStart?: KeepRunStart | undefined);
|
|
45
|
+
runStart?: KeepRunStart | undefined, recording?: FeatureRecording | undefined);
|
|
44
46
|
/** The scenario's tags that do not fit the running environment (read once per environment session; unknown: none). */
|
|
45
47
|
notFor(tags: readonly string[]): Promise<readonly UnmetCondition[]>;
|
|
46
48
|
sentences(): SentenceView[];
|
|
@@ -28,13 +28,14 @@ export class LabFeatureSession {
|
|
|
28
28
|
stepChecks;
|
|
29
29
|
clock;
|
|
30
30
|
runStart;
|
|
31
|
+
recording;
|
|
31
32
|
begun = new Map();
|
|
32
33
|
constructor(state, runScenario, manageActors, redactor, ids, options, conditions,
|
|
33
34
|
fixtures,
|
|
34
35
|
isolation,
|
|
35
36
|
stepChecks,
|
|
36
37
|
clock,
|
|
37
|
-
runStart) {
|
|
38
|
+
runStart, recording) {
|
|
38
39
|
this.state = state;
|
|
39
40
|
this.runScenario = runScenario;
|
|
40
41
|
this.manageActors = manageActors;
|
|
@@ -47,6 +48,7 @@ export class LabFeatureSession {
|
|
|
47
48
|
this.stepChecks = stepChecks;
|
|
48
49
|
this.clock = clock;
|
|
49
50
|
this.runStart = runStart;
|
|
51
|
+
this.recording = recording;
|
|
50
52
|
}
|
|
51
53
|
async notFor(tags) {
|
|
52
54
|
return this.conditions ? this.conditions.unmet(tags) : [];
|
|
@@ -83,7 +85,7 @@ export class LabFeatureSession {
|
|
|
83
85
|
tags: request.tags ?? [],
|
|
84
86
|
...(request.fixtureGroup !== undefined ? { group: request.fixtureGroup } : {}),
|
|
85
87
|
...(this.options.fixtures ? { policy: this.options.fixtures } : {}),
|
|
86
|
-
setupSession: (fixtureSetup) => new LabFeatureSession(this.state, this.runScenario, this.manageActors, this.redactor, this.ids, { binding: "manual", fixtureSetup, ...(this.options.surface ? { surface: this.options.surface } : {}), ...(this.options.surfaceConditions ? { surfaceConditions: this.options.surfaceConditions } : {}) }, this.conditions, this.fixtures, this.isolation, this.stepChecks, this.clock),
|
|
88
|
+
setupSession: (fixtureSetup) => new LabFeatureSession(this.state, this.runScenario, this.manageActors, this.redactor, this.ids, { binding: "manual", fixtureSetup, ...(this.options.surface ? { surface: this.options.surface } : {}), ...(this.options.surfaceConditions ? { surfaceConditions: this.options.surfaceConditions } : {}) }, this.conditions, this.fixtures, this.isolation, this.stepChecks, this.clock, undefined, this.recording),
|
|
87
89
|
});
|
|
88
90
|
const start = fixture || isolatedScenario(request.tags ?? []) ? undefined : await this.runStart?.execute(this.ids.next());
|
|
89
91
|
const kept = start && "kept" in start ? start.kept : undefined;
|
|
@@ -167,6 +169,7 @@ export class LabFeatureSession {
|
|
|
167
169
|
...(race ? { race: race.jitter } : {}),
|
|
168
170
|
...(request.outputs?.length ? { outputs: request.outputs } : {}),
|
|
169
171
|
...(this.clock ? { clock: this.clock } : {}),
|
|
172
|
+
...(this.recording ? { recording: this.recording } : {}),
|
|
170
173
|
...(request.hosts && Object.keys(request.hosts).length ? { hosts: request.hosts } : {}),
|
|
171
174
|
});
|
|
172
175
|
this.begun.set(request.name, scenario);
|
|
@@ -9,6 +9,7 @@ import type { PrepareScenarioFixture } from "../fixtures/prepare-scenario-fixtur
|
|
|
9
9
|
import type { IsolateScenario } from "../fixtures/isolate-scenario.js";
|
|
10
10
|
import type { KeepRunStart } from "../fixtures/keep-run-start.js";
|
|
11
11
|
import type { StepChecks } from "./step-checks.js";
|
|
12
|
+
import type { FeatureRecording } from "./feature-recording.js";
|
|
12
13
|
export declare class RunFeatureSteps {
|
|
13
14
|
private readonly state;
|
|
14
15
|
private readonly runScenario;
|
|
@@ -26,6 +27,7 @@ export declare class RunFeatureSteps {
|
|
|
26
27
|
private readonly clock?;
|
|
27
28
|
/** "Keep each fork run's starting state" (5 October 2026). */
|
|
28
29
|
private readonly runStart?;
|
|
30
|
+
private readonly recording?;
|
|
29
31
|
constructor(state: LabState, runScenario: RunScenario, manageActors: ManageActors, redactor: Redactor, ids: IdGenerator, conditions?: ReadScenarioConditions | undefined,
|
|
30
32
|
/** Restores the fixture a scenario's tags name (`@fixture:<name>`) before it is planned (backlog 1b). */
|
|
31
33
|
fixtures?: PrepareScenarioFixture | undefined,
|
|
@@ -36,6 +38,6 @@ export declare class RunFeatureSteps {
|
|
|
36
38
|
/** Times of new actors created ahead of their steps (scenario-actor-creation.ts). */
|
|
37
39
|
clock?: Clock | undefined,
|
|
38
40
|
/** "Keep each fork run's starting state" (5 October 2026). */
|
|
39
|
-
runStart?: KeepRunStart | undefined);
|
|
41
|
+
runStart?: KeepRunStart | undefined, recording?: FeatureRecording | undefined);
|
|
40
42
|
open(options?: FeatureSessionOptions): LabFeatureSession;
|
|
41
43
|
}
|
|
@@ -13,12 +13,13 @@ export class RunFeatureSteps {
|
|
|
13
13
|
stepChecks;
|
|
14
14
|
clock;
|
|
15
15
|
runStart;
|
|
16
|
+
recording;
|
|
16
17
|
constructor(state, runScenario, manageActors, redactor, ids, conditions,
|
|
17
18
|
fixtures,
|
|
18
19
|
isolation,
|
|
19
20
|
stepChecks,
|
|
20
21
|
clock,
|
|
21
|
-
runStart) {
|
|
22
|
+
runStart, recording) {
|
|
22
23
|
this.state = state;
|
|
23
24
|
this.runScenario = runScenario;
|
|
24
25
|
this.manageActors = manageActors;
|
|
@@ -30,8 +31,9 @@ export class RunFeatureSteps {
|
|
|
30
31
|
this.stepChecks = stepChecks;
|
|
31
32
|
this.clock = clock;
|
|
32
33
|
this.runStart = runStart;
|
|
34
|
+
this.recording = recording;
|
|
33
35
|
}
|
|
34
36
|
open(options = {}) {
|
|
35
|
-
return new LabFeatureSession(this.state, this.runScenario, this.manageActors, this.redactor, this.ids, options, this.conditions, this.fixtures, this.isolation, this.stepChecks, this.clock, this.runStart);
|
|
37
|
+
return new LabFeatureSession(this.state, this.runScenario, this.manageActors, this.redactor, this.ids, options, this.conditions, this.fixtures, this.isolation, this.stepChecks, this.clock, this.runStart, this.recording);
|
|
36
38
|
}
|
|
37
39
|
}
|
|
@@ -105,7 +105,11 @@ export function labRuntimeUseCases(ports) {
|
|
|
105
105
|
const stepChecks = new StepChecks(state, frameSources, ports.stepChecks ?? {}, redactor);
|
|
106
106
|
const manageLabSettings = new ManageLabSettings(ports.labSettings);
|
|
107
107
|
const keepRunStart = new KeepRunStart(state, ports.fixtures, manageLabSettings, saveFixture, ports.clock);
|
|
108
|
-
const runFeatureSteps = new RunFeatureSteps(state, runScenario, manageActors, redactor, ports.ids, readScenarioConditions, prepareScenarioFixture, isolateScenario, stepChecks, ports.clock, keepRunStart
|
|
108
|
+
const runFeatureSteps = new RunFeatureSteps(state, runScenario, manageActors, redactor, ports.ids, readScenarioConditions, prepareScenarioFixture, isolateScenario, stepChecks, ports.clock, keepRunStart, {
|
|
109
|
+
stepStarted: (scenario, at) => recordRuns.stepStarted(scenario, at),
|
|
110
|
+
stepFinished: (scenario, step, identities) => recordRuns.stepFinished(scenario, step, identities),
|
|
111
|
+
scenarioEnded: (scenario) => recordRuns.scenarioEnded(scenario),
|
|
112
|
+
});
|
|
109
113
|
const manageBreakpoints = new ManageBreakpoints(listFeatures, ports.breakpoints);
|
|
110
114
|
const lintLab = new LintLab(state, listFeatures, ports.featureRunner, redactor, ports.clientBundles);
|
|
111
115
|
const runFeatures = new RunFeatures(state, ports.featureRunner, listFeatures, ports.clock, ports.ids, redactor, judgeNegativeControl, policy, runFeatureSteps, readScenarioConditions, events, manageBreakpoints, lintLab);
|
|
@@ -37,6 +37,13 @@ export declare function waitLabelHtml(label: string): string;
|
|
|
37
37
|
export declare function footerHtml(text: string): string;
|
|
38
38
|
/** A failed run's banner, on every frame: the demo never reads as a pass. */
|
|
39
39
|
export declare function failureBannerHtml(text: string): string;
|
|
40
|
+
/** A complete held frame. The app keeps a 1280×720 area; all text lives in the separate band below it. */
|
|
41
|
+
export declare function demoFrameHtml(image: string, caption: {
|
|
42
|
+
number: number;
|
|
43
|
+
keyword: string;
|
|
44
|
+
text: string;
|
|
45
|
+
note?: string;
|
|
46
|
+
}, phase: GherkinPhase, footer: string, banner?: string): string;
|
|
40
47
|
export interface DemoSlide {
|
|
41
48
|
title: string;
|
|
42
49
|
feature: string;
|
|
@@ -60,6 +67,7 @@ export interface DemoSlide {
|
|
|
60
67
|
recordedAt: string;
|
|
61
68
|
/** The last settled screen, as a data URL. */
|
|
62
69
|
image?: string;
|
|
70
|
+
imageNote?: string;
|
|
63
71
|
banner?: string;
|
|
64
72
|
}
|
|
65
73
|
/** The demo's slide (1600×900): the scenario, each step and its outcome, who and where, and the last screen. */
|
|
@@ -56,6 +56,16 @@ export function failureBannerHtml(text) {
|
|
|
56
56
|
return page(`body{background:transparent;height:100vh}
|
|
57
57
|
.banner{position:absolute;top:0;left:0;right:0;background:var(--danger);color:#fff;font-weight:640;font-size:18px;text-align:center;padding:10px 20px}`, `<div class="banner">${escapeHtml(text)}</div>`);
|
|
58
58
|
}
|
|
59
|
+
export function demoFrameHtml(image, caption, phase, footer, banner) {
|
|
60
|
+
return page(`body{width:1280px;height:960px;background:var(--canvas);display:grid;grid-template-rows:720px 240px}
|
|
61
|
+
.app{width:1280px;height:720px;display:flex;align-items:center;justify-content:center;background:var(--canvas);overflow:hidden}
|
|
62
|
+
.app img{width:100%;height:100%;object-fit:contain;display:block}
|
|
63
|
+
.band{height:240px;border-top:1px solid var(--line);padding:12px 24px;display:flex;flex-direction:column;gap:7px;overflow:hidden;background:var(--surface)}
|
|
64
|
+
.row{display:flex;gap:12px;align-items:baseline;font-size:23px;line-height:1.25}
|
|
65
|
+
.n{font:600 16px var(--font-mono);color:var(--secondary)}.k{font-weight:700;color:var(--phase-${phase})}.t{font-weight:540}
|
|
66
|
+
.note{font-size:17px;line-height:1.2;color:var(--secondary)}.failure{font-size:17px;line-height:1.2;color:var(--danger);font-weight:700}
|
|
67
|
+
.footer{margin-top:auto;font-size:12px;line-height:1.2;color:var(--secondary)}.brand{color:var(--accent);font-weight:700}`, `<div class="app"><img src="${escapeHtml(image)}" alt="Recorded app state"></div><div class="band">${banner ? `<div class="failure">${escapeHtml(banner)}</div>` : ""}<div class="row"><span class="n">${caption.number}</span><span class="k">${escapeHtml(caption.keyword)}</span><span class="t">${escapeHtml(caption.text)}</span></div>${caption.note ? `<div class="note">${escapeHtml(caption.note)}</div>` : ""}<div class="footer"><span class="brand">product·lab</span> · ${escapeHtml(footer)} · recorded still</div></div>`);
|
|
68
|
+
}
|
|
59
69
|
const seconds = (value) => `${value < 10 ? value.toFixed(1) : value.toFixed(0)} s`;
|
|
60
70
|
export function slideHtml(slide) {
|
|
61
71
|
const phases = gherkinPhases(slide.steps.map((step) => step.keyword));
|
|
@@ -86,5 +96,5 @@ dl{display:grid;grid-template-columns:120px 1fr;gap:6px 14px;margin:28px 0 0;fon
|
|
|
86
96
|
.shot img{max-width:100%;max-height:786px;display:block}
|
|
87
97
|
.empty{color:var(--secondary);padding:40px;font-size:15px}
|
|
88
98
|
.foot{font-size:12px;color:var(--secondary);margin-top:20px}
|
|
89
|
-
.banner{grid-column:1/-1;background:var(--danger);color:#fff;border-radius:var(--radius-md);padding:10px 16px;font-weight:640;margin:-28px 0 0}`, `${slide.banner ? `<div class="banner">${escapeHtml(slide.banner)}</div>` : ""}<section><div class="brand">product·lab</div><h1>${escapeHtml(slide.title)}</h1><div class="feature">${escapeHtml(slide.feature)}</div><span class="status ${slide.status}">${slide.status === "passed" ? "Passed" : "Failed"}</span><ol>${rows}</ol><dl>${facts.map(([name, value]) => `<dt>${escapeHtml(name)}</dt><dd>${escapeHtml(value)}</dd>`).join("")}</dl><div class="foot">Recorded run ${escapeHtml(slide.recording)}, ${escapeHtml(slide.recordedAt)}. Screens are the apps as they painted themselves
|
|
99
|
+
.banner{grid-column:1/-1;background:var(--danger);color:#fff;border-radius:var(--radius-md);padding:10px 16px;font-weight:640;margin:-28px 0 0}`, `${slide.banner ? `<div class="banner">${escapeHtml(slide.banner)}</div>` : ""}<section><div class="brand">product·lab</div><h1>${escapeHtml(slide.title)}</h1><div class="feature">${escapeHtml(slide.feature)}</div><span class="status ${slide.status}">${slide.status === "passed" ? "Passed" : "Failed"}</span><ol>${rows}</ol><dl>${facts.map(([name, value]) => `<dt>${escapeHtml(name)}</dt><dd>${escapeHtml(value)}</dd>`).join("")}</dl><div class="foot">Recorded run ${escapeHtml(slide.recording)}, ${escapeHtml(slide.recordedAt)}. Screens are the apps as they painted themselves.${slide.imageNote ? ` ${escapeHtml(slide.imageNote)}` : ""}</div></section><div class="shot">${slide.image ? `<img src="${slide.image}" alt="${slide.imageNote ? "Last available recorded frame" : "The last screen"}">` : `<div class="empty">No screenshot of the last screen was recorded.</div>`}</div>`);
|
|
90
100
|
}
|
|
@@ -26,7 +26,5 @@ export declare class DemoRecording {
|
|
|
26
26
|
private video;
|
|
27
27
|
/** The scenario's Gherkin, as the .feature file writes it. */
|
|
28
28
|
private scenario;
|
|
29
|
-
/** The recording's last after-frame of the video's identity (or any), when stored. */
|
|
30
|
-
private lastFrame;
|
|
31
29
|
private failure;
|
|
32
30
|
}
|