mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-25 08:28:02 +00:00
fix(ui): preserve mocked browser proof across reruns (#133918)
* fix(ui): preserve mocked browser proof across reruns Give mocked browser captures run- and attempt-owned directories so reruns preserve earlier screenshots, recordings, and reports. Keep capture helpers and Mantis evidence consumers within the same directory owner. * test(ci): align fixtures with capture allocation and source imports
This commit is contained in:
parent
d6f13a7e08
commit
e8ffa3557d
269 changed files with 3043 additions and 2130 deletions
|
|
@ -41,7 +41,10 @@ When running mocked Control UI/dashboard validation for a user-facing feature, p
|
|||
- After or alongside the focused E2E test, run the mocked Control UI app when available, for example `pnpm dev:ui:mock -- --port <port>`.
|
||||
- Drive Chromium with Playwright against the local mock URL and capture a video plus screenshots for each meaningful state: initial view, interaction input, result state, and final/paginated/selected state.
|
||||
- Use `browser.newContext({ recordVideo: { dir, size }, viewport })`, `page.screenshot({ path })`, and close the context before reporting the video path.
|
||||
- Put artifacts under `.artifacts/control-ui-e2e/<short-feature-name>/` or another clearly named local temp directory, and report the absolute paths in the final answer.
|
||||
- Allocate retained proof with `createControlUiE2eArtifactDir(scope, parentDir?)` from `ui/src/test-helpers/control-ui-e2e-artifacts.ts`. Each call atomically creates a fresh directory and logs its actual path. An explicit parent wins, then the trimmed existing `OPENCLAW_UI_E2E_ARTIFACT_DIR`, then the repository's `.artifacts/control-ui-e2e` parent. Existing custom output controls select parents; do not add or rewrite env vars to enable capture.
|
||||
- Allocate during the test/scenario or `beforeEach`, once per attempt; standalone scripts allocate once per invocation. Pass the owner explicitly to shared capture helpers. Keep the original gates, feature/stage names, viewports, waits, and recording options. Use distinct filenames for distinct stages and keep screenshots, reports, and video together.
|
||||
- Retain successful and failed evidence. Report actual allocated paths, including relocated filename overrides. Manually delete only exact owned directories after review; never clear shared parents before a replay. Disposable build/media fixtures and owned temporary raw video may keep their cleanup. New synthetic captures do not recover overwritten evidence.
|
||||
- Timeout diagnostics use fresh children beneath their existing diagnostic parent. Mantis retains every capture attempt under an invocation-owned directory and refuses to overwrite reports. Real-Gateway suites, `chat-outbox-*`, and `chat-attachment-read-lifecycle` remain separate owners; coordinate before claiming replay-safe retention there.
|
||||
- Treat recording as validation, not only demo capture. If the recorder fails or shows surprising behavior, stop, fix the behavior, add or update a regression test, then rerecord.
|
||||
- If visual proof is blocked, state the exact blocker and still report the textual E2E evidence.
|
||||
|
||||
|
|
@ -74,4 +77,4 @@ When recording an already-running mocked Control UI URL, use a temporary Playwri
|
|||
- Open the mock URL, interact through stable `data-*` selectors or user-facing role selectors, and wait on asserted states instead of relying on fixed sleeps.
|
||||
- Assert both visible UI state and mocked Gateway traffic for request-driven flows. For example, verify the expected count/row is visible and that `sessions.list` was called with the expected `search`, `offset`, and `limit`.
|
||||
- Use short sleeps only after assertions to make the captured video readable.
|
||||
- Store the generated video under `.artifacts/control-ui-e2e/<feature>/`; do not commit it.
|
||||
- Store the generated video in the invocation's fresh allocated directory; do not commit it or remove older captures.
|
||||
|
|
|
|||
107
.github/workflows/mantis-web-ui-chat-proof.yml
vendored
107
.github/workflows/mantis-web-ui-chat-proof.yml
vendored
|
|
@ -113,9 +113,9 @@ jobs:
|
|||
runs-on: blacksmith-8vcpu-ubuntu-2404
|
||||
timeout-minutes: 60
|
||||
outputs:
|
||||
artifact_name: ${{ steps.run_mantis.outputs.artifact_name }}
|
||||
output_dir: ${{ steps.run_mantis.outputs.output_dir }}
|
||||
proof_status: ${{ steps.run_mantis.outputs.proof_status }}
|
||||
artifact_name: ${{ steps.prepare_evidence.outputs.artifact_name }}
|
||||
output_dir: ${{ steps.prepare_evidence.outputs.output_dir }}
|
||||
proof_status: ${{ steps.build_evidence.outputs.proof_status }}
|
||||
steps:
|
||||
- name: Checkout harness ref
|
||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
|
|
@ -123,6 +123,18 @@ jobs:
|
|||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Allocate invocation evidence directory
|
||||
id: prepare_evidence
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
output_parent="${GITHUB_WORKSPACE}/.artifacts/qa-e2e/mantis/web-ui-chat-proof"
|
||||
mkdir -p "$output_parent"
|
||||
root="$(mktemp -d "$output_parent/run.XXXXXX")"
|
||||
echo "Retained Mantis invocation evidence: $root"
|
||||
echo "artifact_name=mantis-web-ui-chat-proof-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" >> "$GITHUB_OUTPUT"
|
||||
echo "output_dir=${root}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Prepare Git owner
|
||||
uses: openclaw/openclaw/.github/actions/git-owner@dd4528b6393e7d00063067a080ca7241b48ce475
|
||||
|
||||
|
|
@ -140,71 +152,68 @@ jobs:
|
|||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
worktree_root=".artifacts/qa-e2e/mantis/web-ui-chat-proof-worktrees"
|
||||
mkdir -p "$worktree_root"
|
||||
python3 -I -S "$CI_GIT_OWNER" --checkout-git 0 worktree add --detach "$worktree_root/candidate" "$CANDIDATE_SHA"
|
||||
pnpm --dir "$worktree_root/candidate" install --frozen-lockfile --prefer-offline
|
||||
root="${{ steps.prepare_evidence.outputs.output_dir }}"
|
||||
{
|
||||
worktree_root=".artifacts/qa-e2e/mantis/web-ui-chat-proof-worktrees"
|
||||
mkdir -p "$worktree_root"
|
||||
python3 -I -S "$CI_GIT_OWNER" --checkout-git 0 worktree add --detach "$worktree_root/candidate" "$CANDIDATE_SHA"
|
||||
pnpm --dir "$worktree_root/candidate" install --frozen-lockfile --prefer-offline
|
||||
} 2>&1 | tee "$root/setup.log"
|
||||
|
||||
- name: Run web UI chat proof
|
||||
id: run_mantis
|
||||
env:
|
||||
CANDIDATE_REF: ${{ needs.resolve_request.outputs.candidate_ref }}
|
||||
CANDIDATE_SHA: ${{ needs.validate_candidate.outputs.candidate_revision }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
artifact_name="mantis-web-ui-chat-proof-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
root="${{ steps.prepare_evidence.outputs.output_dir }}"
|
||||
candidate_repo="$(pwd)/.artifacts/qa-e2e/mantis/web-ui-chat-proof-worktrees/candidate"
|
||||
output_rel=".artifacts/qa-e2e/mantis/web-ui-chat-proof"
|
||||
root="$candidate_repo/$output_rel"
|
||||
mkdir -p "$root"
|
||||
echo "artifact_name=${artifact_name}" >> "$GITHUB_OUTPUT"
|
||||
echo "output_dir=${root}" >> "$GITHUB_OUTPUT"
|
||||
{
|
||||
install -D -m 0644 \
|
||||
"${GITHUB_WORKSPACE}/ui/src/e2e/mantis-chat-proof.e2e.test.ts" \
|
||||
"$candidate_repo/ui/src/e2e/mantis-chat-proof.e2e.test.ts"
|
||||
install -D -m 0644 \
|
||||
"${GITHUB_WORKSPACE}/ui/src/test-helpers/control-ui-e2e.ts" \
|
||||
"$candidate_repo/ui/src/test-helpers/control-ui-e2e.ts"
|
||||
install -D -m 0644 \
|
||||
"${GITHUB_WORKSPACE}/ui/src/test-helpers/control-ui-e2e-artifacts.ts" \
|
||||
"$candidate_repo/ui/src/test-helpers/control-ui-e2e-artifacts.ts"
|
||||
|
||||
install -D -m 0644 \
|
||||
"${GITHUB_WORKSPACE}/ui/src/e2e/mantis-chat-proof.e2e.test.ts" \
|
||||
"$candidate_repo/ui/src/e2e/mantis-chat-proof.e2e.test.ts"
|
||||
install -D -m 0644 \
|
||||
"${GITHUB_WORKSPACE}/ui/src/test-helpers/control-ui-e2e.ts" \
|
||||
"$candidate_repo/ui/src/test-helpers/control-ui-e2e.ts"
|
||||
|
||||
cd "$candidate_repo"
|
||||
node --import tsx scripts/ensure-playwright-chromium.mts
|
||||
|
||||
set +e
|
||||
OPENCLAW_MANTIS_WEB_UI_CHAT_OUTPUT_DIR="$root" \
|
||||
node scripts/run-vitest.mjs run \
|
||||
--config test/vitest/vitest.ui-e2e.config.ts \
|
||||
--configLoader runner \
|
||||
ui/src/e2e/mantis-chat-proof.e2e.test.ts \
|
||||
2>&1 | tee "$root/vitest.log"
|
||||
vitest_exit="${PIPESTATUS[0]}"
|
||||
set -e
|
||||
|
||||
proof_status="pass"
|
||||
if [[ "$vitest_exit" -ne 0 ]]; then
|
||||
proof_status="fail"
|
||||
fi
|
||||
echo "proof_status=${proof_status}" >> "$GITHUB_OUTPUT"
|
||||
cd "$candidate_repo"
|
||||
node --import tsx scripts/ensure-playwright-chromium.mts
|
||||
OPENCLAW_MANTIS_WEB_UI_CHAT_OUTPUT_DIR="$root" \
|
||||
OPENCLAW_UI_E2E_ARTIFACT_DIR="$root" \
|
||||
node scripts/run-vitest.mjs run \
|
||||
--config test/vitest/vitest.ui-e2e.config.ts \
|
||||
--configLoader runner \
|
||||
ui/src/e2e/mantis-chat-proof.e2e.test.ts
|
||||
} 2>&1 | tee "$root/vitest.log"
|
||||
|
||||
- name: Build invocation evidence report
|
||||
id: build_evidence
|
||||
if: ${{ always() && steps.prepare_evidence.outputs.output_dir != '' }}
|
||||
env:
|
||||
CANDIDATE_REF: ${{ needs.resolve_request.outputs.candidate_ref }}
|
||||
CANDIDATE_SHA: ${{ needs.validate_candidate.outputs.candidate_revision }}
|
||||
PROOF_STATUS: ${{ steps.run_mantis.outcome == 'success' && 'pass' || 'fail' }}
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
root="${{ steps.prepare_evidence.outputs.output_dir }}"
|
||||
echo "proof_status=${PROOF_STATUS}" >> "$GITHUB_OUTPUT"
|
||||
node "${GITHUB_WORKSPACE}/scripts/mantis/build-web-ui-chat-evidence.mjs" \
|
||||
--output-dir "$root" \
|
||||
--candidate-ref "$CANDIDATE_REF" \
|
||||
--candidate-sha "$CANDIDATE_SHA" \
|
||||
--status "$proof_status"
|
||||
--status "$PROOF_STATUS"
|
||||
|
||||
cat "$root/mantis-report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
if [[ "$vitest_exit" -ne 0 ]]; then
|
||||
exit "$vitest_exit"
|
||||
fi
|
||||
|
||||
- name: Upload Mantis web UI chat artifacts
|
||||
if: ${{ always() && steps.run_mantis.outputs.output_dir != '' }}
|
||||
if: ${{ always() && steps.prepare_evidence.outputs.output_dir != '' }}
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||
with:
|
||||
name: ${{ steps.run_mantis.outputs.artifact_name }}
|
||||
path: ${{ steps.run_mantis.outputs.output_dir }}
|
||||
name: ${{ steps.prepare_evidence.outputs.artifact_name }}
|
||||
path: ${{ steps.prepare_evidence.outputs.output_dir }}
|
||||
retention-days: 14
|
||||
if-no-files-found: error
|
||||
|
||||
|
|
|
|||
|
|
@ -330,6 +330,40 @@ not sweep old directories or infer ownership from names, ages, or PIDs.
|
|||
- Base Vitest config defaults to `pool: "threads"` and `isolate: false`, with the shared non-isolated runner enabled across repo configs.
|
||||
- `pnpm test:channels` runs `vitest.channels.config.ts`.
|
||||
|
||||
### Retained mocked Control UI proof
|
||||
|
||||
Ordinary mocked browser screenshots, recordings, and reports use fresh directories
|
||||
for each test attempt or standalone capture invocation. The Node-only
|
||||
`createControlUiE2eArtifactDir(scope, parentDir?)` helper in
|
||||
`ui/src/test-helpers/control-ui-e2e-artifacts.ts` prints the actual allocated path.
|
||||
An explicit parent wins; otherwise it uses the trimmed existing
|
||||
`OPENCLAW_UI_E2E_ARTIFACT_DIR`, then the repository's `.artifacts/control-ui-e2e`
|
||||
parent. Existing feature-specific directory controls and script output arguments
|
||||
select parents, with unique children beneath them. Explicit screenshot filename
|
||||
controls preserve the basename and print the relocated path.
|
||||
|
||||
Keep capture gates independent from allocation: `OPENCLAW_CAPTURE_UI_PROOF`,
|
||||
`OPENCLAW_UI_E2E_RECORD`, and output-presence gates retain their existing meanings.
|
||||
Allocate during scenario execution or `beforeEach`, and pass the same owner to
|
||||
shared capture helpers so screenshots, reports, and video stay together. Distinguish
|
||||
stage names within an attempt. Close the browser context before finalizing video.
|
||||
|
||||
Successful and failed evidence is retained. Cleanup is manual: remove only exact
|
||||
directories that you own and have finished reviewing. Never recursively delete
|
||||
the shared parent before a replay. Disposable build/media fixtures and temporary
|
||||
raw video have their own cleanup. New captures cannot recover overwritten evidence;
|
||||
do not describe a replay as recovery of lost files.
|
||||
|
||||
Timeout diagnostics allocate fresh children beneath the existing
|
||||
`OPENCLAW_UI_E2E_DIAGNOSTIC_DIR` or default timeout directory, keeping each PNG and
|
||||
JSON report together. Mantis allocates an invocation directory for setup logs,
|
||||
capture attempts, and its report; the builder preserves each attempt's relative
|
||||
paths and refuses to overwrite an existing report.
|
||||
|
||||
Separate output owners remain: real-Gateway suites, `chat-outbox-*`, and
|
||||
`chat-attachment-read-lifecycle`. Do not assume those owners have the ordinary
|
||||
mocked proof retention guarantee.
|
||||
|
||||
## Gateway and E2E
|
||||
|
||||
- Gateway tests are included in the untargeted `pnpm test` full suite; run them alone with `pnpm test:gateway`.
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
#!/usr/bin/env node
|
||||
// Captures composer mic-hover proof shots: idle vs hovered talk control, plus
|
||||
// the mic button's x position in both states (the hover-shift regression).
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium } from "playwright";
|
||||
// Captures composer mic-hover proof shots: idle vs hovered talk control, plus
|
||||
// the mic button's x position in both states (the hover-shift regression).
|
||||
import { createControlUiE2eArtifactDir } from "../ui/src/test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
|
|
@ -21,7 +21,8 @@ function readOption(name: string): string | undefined {
|
|||
return index >= 0 ? process.argv[index + 1] : undefined;
|
||||
}
|
||||
|
||||
const outputDir = path.resolve(
|
||||
const outputDir = createControlUiE2eArtifactDir(
|
||||
"composer-mic-hover-proof",
|
||||
readOption("output-dir") ?? ".artifacts/control-ui-e2e/composer-mic-hover-proof",
|
||||
);
|
||||
const label = readOption("label") ?? "after";
|
||||
|
|
@ -30,7 +31,6 @@ if (!canRunPlaywrightChromium(executablePath)) {
|
|||
throw new Error(`Playwright Chromium is unavailable at ${executablePath}`);
|
||||
}
|
||||
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
const server = await startControlUiE2eServer(undefined, { source: true });
|
||||
const browser = await chromium.launch({ executablePath });
|
||||
const context = await browser.newContext({
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
#!/usr/bin/env node
|
||||
import path from "node:path";
|
||||
import { chromium } from "playwright";
|
||||
// Captures webchat proof that a CLI harness-injected user turn renders as a
|
||||
// collapsed system notice while the operator's own message keeps its bubble.
|
||||
// Usage: node --import tsx scripts/capture-injected-turn-notice-proof.mts [--mode after|before]
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium } from "playwright";
|
||||
import { createControlUiE2eArtifactDir } from "../ui/src/test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
|
|
@ -26,7 +26,8 @@ const mode = readOption("mode") ?? "after";
|
|||
if (mode !== "after" && mode !== "before") {
|
||||
throw new Error(`Expected --mode after|before, received ${mode}`);
|
||||
}
|
||||
const outputDir = path.resolve(
|
||||
const outputDir = createControlUiE2eArtifactDir(
|
||||
"injected-turn-notice-proof",
|
||||
readOption("output-dir") ?? ".artifacts/control-ui-e2e/injected-turn-notice-proof",
|
||||
);
|
||||
|
||||
|
|
@ -78,7 +79,6 @@ if (!canRunPlaywrightChromium(executablePath)) {
|
|||
throw new Error(`Playwright Chromium is unavailable at ${executablePath}`);
|
||||
}
|
||||
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
const server = await startControlUiE2eServer(undefined, { source: true });
|
||||
const browser = await chromium.launch({ executablePath });
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
#!/usr/bin/env node
|
||||
import path from "node:path";
|
||||
import { chromium } from "playwright";
|
||||
// Captures chat model-picker proof shots: the open picker inheriting the agent
|
||||
// default, the same picker with a session override, and the picker after a
|
||||
// failed catalog refresh. Also reports the x offset between the provider
|
||||
// heading label and the model row name (the row-alignment change).
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium } from "playwright";
|
||||
import { createControlUiE2eArtifactDir } from "../ui/src/test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
|
|
@ -23,7 +23,8 @@ function readOption(name: string): string | undefined {
|
|||
return index >= 0 ? process.argv[index + 1] : undefined;
|
||||
}
|
||||
|
||||
const outputDir = path.resolve(
|
||||
const outputDir = createControlUiE2eArtifactDir(
|
||||
"model-picker-proof",
|
||||
readOption("output-dir") ?? ".artifacts/control-ui-e2e/model-picker-proof",
|
||||
);
|
||||
const label = readOption("label") ?? "after";
|
||||
|
|
@ -46,7 +47,6 @@ const models = [
|
|||
{ id: "claude-opus-4-6", name: "Claude Opus 4.6", provider: "anthropic", contextWindow: 200_000 },
|
||||
];
|
||||
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
const server = await startControlUiE2eServer(undefined, { source: true });
|
||||
const browser = await chromium.launch({ executablePath });
|
||||
const context = await browser.newContext({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Page } from "playwright";
|
||||
import { createControlUiE2eArtifactDir } from "../ui/src/test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
|
|
@ -112,7 +112,8 @@ const ungroupedSessions = [
|
|||
];
|
||||
|
||||
const mode = readMode();
|
||||
const outputDir = path.resolve(
|
||||
const outputDir = createControlUiE2eArtifactDir(
|
||||
"session-toolbar-proof",
|
||||
readOption("output-dir") ?? ".artifacts/control-ui-e2e/session-toolbar-proof",
|
||||
);
|
||||
const executablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
|
||||
|
|
@ -120,7 +121,6 @@ if (!canRunPlaywrightChromium(executablePath)) {
|
|||
throw new Error(`Playwright Chromium is unavailable at ${executablePath}`);
|
||||
}
|
||||
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
const server = await startControlUiE2eServer(undefined, { source: true });
|
||||
const browser = await chromium.launch({ executablePath });
|
||||
const captured: string[] = [];
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
#!/usr/bin/env node
|
||||
// Captures placement, inventory, and context-meter proof in both themes.
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium } from "playwright";
|
||||
import { createControlUiE2eArtifactDir } from "../ui/src/test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
|
|
@ -12,9 +12,11 @@ import {
|
|||
} from "../ui/src/test-helpers/control-ui-e2e.ts";
|
||||
|
||||
const captureLabel = process.argv[2] ?? "after";
|
||||
const outputDir = path.resolve(process.argv[3] ?? "/tmp/node-slot-pips-proof");
|
||||
const outputDir = createControlUiE2eArtifactDir(
|
||||
"node-slot-pips-proof",
|
||||
process.argv[3] ?? "/tmp/node-slot-pips-proof",
|
||||
);
|
||||
const remoteExec = process.argv[4] === "remote-exec";
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
const executablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
|
||||
if (!canRunPlaywrightChromium(executablePath)) {
|
||||
throw new Error(`Playwright Chromium unavailable at ${executablePath}`);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env node
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium } from "playwright";
|
||||
import { createControlUiE2eArtifactDir } from "../ui/src/test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
resolvePlaywrightChromiumExecutablePath,
|
||||
|
|
@ -22,13 +22,15 @@ function readOption(name: string): string | undefined {
|
|||
}
|
||||
|
||||
const baseUrl = new URL(readOption("base-url") ?? DEFAULT_BASE_URL);
|
||||
const outputDir = path.resolve(readOption("output-dir") ?? DEFAULT_OUTPUT_DIR);
|
||||
const outputDir = createControlUiE2eArtifactDir(
|
||||
"workboard-proof",
|
||||
readOption("output-dir") ?? DEFAULT_OUTPUT_DIR,
|
||||
);
|
||||
const executablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
|
||||
if (!canRunPlaywrightChromium(executablePath)) {
|
||||
throw new Error(`Playwright Chromium is unavailable at ${executablePath}`);
|
||||
}
|
||||
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
const browser = await chromium.launch({ executablePath });
|
||||
const context = await browser.newContext({
|
||||
colorScheme: "light",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
#!/usr/bin/env node
|
||||
// Builds a Mantis evidence manifest from Control UI web chat proof artifacts.
|
||||
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
|
|
@ -35,26 +35,27 @@ function normalizeStatus(value) {
|
|||
throw new Error(`Unsupported web UI chat proof status: ${value}`);
|
||||
}
|
||||
|
||||
function artifactEntry({ inline = false, kind, label, path: artifactPath, required, targetPath }) {
|
||||
function artifactEntry({ inline = false, kind, label, path: artifactPath, required }) {
|
||||
return {
|
||||
kind,
|
||||
lane: "candidate",
|
||||
label,
|
||||
path: artifactPath,
|
||||
targetPath,
|
||||
targetPath: artifactPath,
|
||||
required,
|
||||
...(inline ? { alt: label, inline: true, width: 900 } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildWebUiChatEvidenceManifest({ candidateRef, candidateSha, status }) {
|
||||
function buildWebUiChatEvidenceManifest({ candidateRef, candidateSha, status, captures }) {
|
||||
const passed = status === "pass";
|
||||
return {
|
||||
schemaVersion: 2,
|
||||
id: "web-ui-chat-proof",
|
||||
title: "Mantis Web UI Chat Proof",
|
||||
summary:
|
||||
"Mantis ran the OpenClaw Control UI chat proof against the candidate ref, sent a message through the mocked Gateway, rendered the final reply in the browser, and captured browser artifacts for review.",
|
||||
summary: passed
|
||||
? "Mantis ran the OpenClaw Control UI chat proof against the candidate ref, sent a message through the mocked Gateway, rendered the final reply in the browser, and captured browser artifacts for review."
|
||||
: "Mantis could not complete the Control UI chat proof. Retained attempt artifacts and logs describe the failure.",
|
||||
scenario: "web-ui-chat-proof",
|
||||
comparison: {
|
||||
candidate: {
|
||||
|
|
@ -69,34 +70,38 @@ export function buildWebUiChatEvidenceManifest({ candidateRef, candidateSha, sta
|
|||
pass: passed,
|
||||
},
|
||||
artifacts: [
|
||||
artifactEntry({
|
||||
inline: true,
|
||||
kind: "desktopScreenshot",
|
||||
label: "Control UI web chat proof",
|
||||
path: "web-ui-chat.png",
|
||||
required: passed,
|
||||
targetPath: "web-ui-chat.png",
|
||||
}),
|
||||
artifactEntry({
|
||||
kind: "fullVideo",
|
||||
label: "Control UI web chat recording",
|
||||
path: "web-ui-chat.webm",
|
||||
required: false,
|
||||
targetPath: "web-ui-chat.webm",
|
||||
}),
|
||||
artifactEntry({
|
||||
kind: "metadata",
|
||||
label: "Control UI web chat proof metadata",
|
||||
path: "web-ui-chat-proof.json",
|
||||
required: passed,
|
||||
targetPath: "web-ui-chat-proof.json",
|
||||
}),
|
||||
...captures.flatMap(({ directory, complete }) => [
|
||||
artifactEntry({
|
||||
inline: true,
|
||||
kind: "desktopScreenshot",
|
||||
label: `Control UI web chat proof (${directory})`,
|
||||
path: `${directory}/web-ui-chat.png`,
|
||||
required: passed && complete,
|
||||
}),
|
||||
artifactEntry({
|
||||
kind: "fullVideo",
|
||||
label: `Control UI web chat recording (${directory})`,
|
||||
path: `${directory}/web-ui-chat.webm`,
|
||||
required: false,
|
||||
}),
|
||||
artifactEntry({
|
||||
kind: "metadata",
|
||||
label: `Control UI web chat proof metadata (${directory})`,
|
||||
path: `${directory}/web-ui-chat-proof.json`,
|
||||
required: passed && complete,
|
||||
}),
|
||||
]),
|
||||
artifactEntry({
|
||||
kind: "metadata",
|
||||
label: "Control UI web chat Vitest log",
|
||||
path: "vitest.log",
|
||||
required: false,
|
||||
targetPath: "vitest.log",
|
||||
}),
|
||||
artifactEntry({
|
||||
kind: "metadata",
|
||||
label: "Control UI web chat setup log",
|
||||
path: "setup.log",
|
||||
required: false,
|
||||
}),
|
||||
{
|
||||
kind: "report",
|
||||
|
|
@ -109,7 +114,7 @@ export function buildWebUiChatEvidenceManifest({ candidateRef, candidateSha, sta
|
|||
};
|
||||
}
|
||||
|
||||
function renderReport({ candidateRef, candidateSha, outputDir, status }) {
|
||||
function renderReport({ candidateRef, candidateSha, outputDir, status, artifacts }) {
|
||||
const artifactStatus = (artifactPath) =>
|
||||
existsSync(path.join(outputDir, artifactPath)) ? "present" : "missing";
|
||||
return [
|
||||
|
|
@ -121,14 +126,16 @@ function renderReport({ candidateRef, candidateSha, outputDir, status }) {
|
|||
"",
|
||||
"## Scenario",
|
||||
"",
|
||||
"OpenClaw Control UI chat was loaded in a browser with the mocked Gateway harness. The proof sends a chat message through the GUI, verifies the `chat.send` request, emits a final Gateway reply, and waits for the reply to render in the web chat thread.",
|
||||
"The scenario loads OpenClaw Control UI chat in a browser with the mocked Gateway harness, sends a chat message through the GUI, verifies the `chat.send` request, emits a final Gateway reply, and waits for the reply to render in the web chat thread.",
|
||||
"",
|
||||
"## Artifacts",
|
||||
"",
|
||||
`- Screenshot: \`web-ui-chat.png\` (${artifactStatus("web-ui-chat.png")})`,
|
||||
`- Recording: \`web-ui-chat.webm\` (${artifactStatus("web-ui-chat.webm")})`,
|
||||
`- Proof metadata: \`web-ui-chat-proof.json\` (${artifactStatus("web-ui-chat-proof.json")})`,
|
||||
`- Vitest log: \`vitest.log\` (${artifactStatus("vitest.log")})`,
|
||||
...artifacts
|
||||
.filter((artifact) => artifact.kind !== "report")
|
||||
.map(
|
||||
(artifact) =>
|
||||
`- ${artifact.label}: \`${artifact.path}\` (${artifactStatus(artifact.path)})`,
|
||||
),
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
|
@ -143,13 +150,35 @@ export function writeWebUiChatEvidence(rawArgs = process.argv.slice(2)) {
|
|||
}
|
||||
const outputDir = path.resolve(args.output_dir);
|
||||
mkdirSync(outputDir, { recursive: true });
|
||||
const reportPath = path.join(outputDir, "mantis-report.md");
|
||||
const manifestPath = path.join(outputDir, "mantis-evidence.json");
|
||||
if (existsSync(reportPath) || existsSync(manifestPath)) {
|
||||
throw new Error("Evidence already exists in --output-dir; use a fresh invocation directory.");
|
||||
}
|
||||
const status = normalizeStatus(args.status);
|
||||
// The workflow owns this fresh invocation root. Keep every attempt, including
|
||||
// incomplete retries; never pick a newest capture from a shared history root.
|
||||
const captures = readdirSync(outputDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isDirectory() && entry.name.startsWith("mantis-chat-proof-"))
|
||||
.map((entry) => entry.name)
|
||||
.toSorted()
|
||||
.map((directory) => ({
|
||||
directory,
|
||||
complete: ["web-ui-chat.png", "web-ui-chat-proof.json"].every((file) =>
|
||||
existsSync(path.join(outputDir, directory, file)),
|
||||
),
|
||||
}));
|
||||
if (status === "pass" && !captures.some((capture) => capture.complete)) {
|
||||
throw new Error(
|
||||
"Passing proof requires a captured screenshot and proof metadata in --output-dir.",
|
||||
);
|
||||
}
|
||||
const manifest = buildWebUiChatEvidenceManifest({
|
||||
candidateRef: args.candidate_ref,
|
||||
candidateSha: args.candidate_sha,
|
||||
status,
|
||||
captures,
|
||||
});
|
||||
const reportPath = path.join(outputDir, "mantis-report.md");
|
||||
writeFileSync(
|
||||
reportPath,
|
||||
renderReport({
|
||||
|
|
@ -157,11 +186,14 @@ export function writeWebUiChatEvidence(rawArgs = process.argv.slice(2)) {
|
|||
candidateSha: args.candidate_sha,
|
||||
outputDir,
|
||||
status,
|
||||
artifacts: manifest.artifacts,
|
||||
}),
|
||||
"utf8",
|
||||
{ encoding: "utf8", flag: "wx" },
|
||||
);
|
||||
const manifestPath = path.join(outputDir, "mantis-evidence.json");
|
||||
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
||||
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, {
|
||||
encoding: "utf8",
|
||||
flag: "wx",
|
||||
});
|
||||
return { manifest, manifestPath, reportPath };
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -115,6 +115,7 @@ export async function runCiGitStep(options: {
|
|||
policy?: string;
|
||||
inlinePolicy?: boolean;
|
||||
step?: string;
|
||||
stepOutputs?: Record<string, Record<string, string>>;
|
||||
env?: Record<string, string>;
|
||||
fetchResults: FetchResult[];
|
||||
cloneResults?: FetchResult[];
|
||||
|
|
@ -409,6 +410,11 @@ def main():`,
|
|||
}),
|
||||
);
|
||||
let run = renderGitTestClock(step.run, clock);
|
||||
for (const [stepId, outputs] of Object.entries(options.stepOutputs ?? {})) {
|
||||
for (const [name, value] of Object.entries(outputs)) {
|
||||
run = run.replaceAll(`\${{ steps.${stepId}.outputs.${name} }}`, value);
|
||||
}
|
||||
}
|
||||
if (externalOwner) {
|
||||
const prepare = parse(readFileSync(".github/actions/git-owner/action.yml", "utf8")) as {
|
||||
runs: { steps: { run?: string }[] };
|
||||
|
|
|
|||
|
|
@ -934,6 +934,12 @@ posixIt.each([
|
|||
},
|
||||
fetchResults: [],
|
||||
worktreeResults: failure ? ["cleanup-failure"] : lanes.map(() => 0),
|
||||
// The runner hands off the earlier allocation's output; keep setup logs
|
||||
// inside this fixture's fresh invocation directory.
|
||||
stepOutputs:
|
||||
workflow === "web-ui-chat-proof"
|
||||
? { prepare_evidence: { output_dir: "${RUNNER_TEMP}" } }
|
||||
: undefined,
|
||||
realClock: true,
|
||||
realDrain: false,
|
||||
poisonPython: true,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
// Mantis web UI chat evidence tests cover manifest generation.
|
||||
// Mantis web UI chat evidence tests cover retained capture-to-publisher identity.
|
||||
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { writeWebUiChatEvidence } from "../../scripts/mantis/build-web-ui-chat-evidence.mjs";
|
||||
import {
|
||||
buildWebUiChatEvidenceManifest,
|
||||
writeWebUiChatEvidence,
|
||||
} from "../../scripts/mantis/build-web-ui-chat-evidence.mjs";
|
||||
loadEvidenceManifest,
|
||||
renderEvidenceComment,
|
||||
} from "../../scripts/mantis/publish-pr-evidence.mjs";
|
||||
|
||||
function withTempDir<T>(fn: (dir: string) => T): T {
|
||||
const dir = mkdtempSync(path.join(tmpdir(), "openclaw-mantis-web-ui-chat-"));
|
||||
|
|
@ -17,89 +18,149 @@ function withTempDir<T>(fn: (dir: string) => T): T {
|
|||
}
|
||||
}
|
||||
|
||||
function captureAttempt(parent: string, marker: string, complete = true): string {
|
||||
const directory = mkdtempSync(path.join(parent, "mantis-chat-proof-"));
|
||||
writeFileSync(path.join(directory, "web-ui-chat.png"), `${marker} screenshot`);
|
||||
writeFileSync(path.join(directory, "web-ui-chat.webm"), `${marker} video`);
|
||||
if (complete) {
|
||||
const metadata = JSON.stringify({ status: "pass" });
|
||||
writeFileSync(path.join(directory, "web-ui-chat-proof.json"), metadata);
|
||||
}
|
||||
return directory;
|
||||
}
|
||||
|
||||
describe("build-web-ui-chat-evidence", () => {
|
||||
it("marks a passing Control UI chat proof as publishable visible Mantis evidence", () => {
|
||||
const manifest = buildWebUiChatEvidenceManifest({
|
||||
candidateRef: "HEAD",
|
||||
candidateSha: "1234567890abcdef1234567890abcdef12345678",
|
||||
status: "pass",
|
||||
});
|
||||
|
||||
expect(manifest).toMatchObject({
|
||||
id: "web-ui-chat-proof",
|
||||
scenario: "web-ui-chat-proof",
|
||||
schemaVersion: 2,
|
||||
comparison: {
|
||||
candidate: {
|
||||
expectationMet: true,
|
||||
fixed: true,
|
||||
ref: "HEAD",
|
||||
sha: "1234567890abcdef1234567890abcdef12345678",
|
||||
status: "pass",
|
||||
},
|
||||
pass: true,
|
||||
},
|
||||
});
|
||||
expect(manifest.artifacts).toContainEqual(
|
||||
expect.objectContaining({
|
||||
inline: true,
|
||||
kind: "desktopScreenshot",
|
||||
path: "web-ui-chat.png",
|
||||
required: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("writes a failing manifest without requiring a missing screenshot", () => {
|
||||
it("publishes every attempt at its exact retained path without mixing invocation roots", () => {
|
||||
withTempDir((dir) => {
|
||||
const priorRoot = mkdtempSync(path.join(dir, "run-"));
|
||||
const priorCapture = captureAttempt(priorRoot, "prior");
|
||||
const prior = writeWebUiChatEvidence(["--output-dir", priorRoot, "--status", "pass"]);
|
||||
const priorReport = readFileSync(prior.reportPath, "utf8");
|
||||
const priorManifest = readFileSync(prior.manifestPath, "utf8");
|
||||
|
||||
const root = mkdtempSync(path.join(dir, "run-"));
|
||||
const incomplete = captureAttempt(root, "incomplete", false);
|
||||
const complete = captureAttempt(root, "complete");
|
||||
writeFileSync(path.join(root, "vitest.log"), "current invocation log");
|
||||
const result = writeWebUiChatEvidence([
|
||||
"--output-dir",
|
||||
dir,
|
||||
root,
|
||||
"--candidate-ref",
|
||||
"main",
|
||||
"--candidate-sha",
|
||||
"abcdef",
|
||||
"1234567890abcdef1234567890abcdef12345678",
|
||||
"--status",
|
||||
"fail",
|
||||
"pass",
|
||||
]);
|
||||
|
||||
expect(existsSync(result.manifestPath)).toBe(true);
|
||||
expect(existsSync(result.reportPath)).toBe(true);
|
||||
const manifest = JSON.parse(readFileSync(result.manifestPath, "utf8"));
|
||||
expect(manifest.comparison.pass).toBe(false);
|
||||
expect(manifest.comparison.candidate.expectationMet).toBe(false);
|
||||
expect(
|
||||
manifest.artifacts.find(
|
||||
(artifact: { path: string }) => artifact.path === "web-ui-chat.png",
|
||||
),
|
||||
).toMatchObject({
|
||||
required: false,
|
||||
const manifest = loadEvidenceManifest(result.manifestPath);
|
||||
expect(manifest).toMatchObject({
|
||||
id: "web-ui-chat-proof",
|
||||
scenario: "web-ui-chat-proof",
|
||||
schemaVersion: 2,
|
||||
comparison: {
|
||||
candidate: { expectationMet: true, fixed: true, ref: "main", status: "pass" },
|
||||
pass: true,
|
||||
},
|
||||
});
|
||||
const screenshots = manifest.artifacts.filter(
|
||||
(artifact) => artifact.kind === "desktopScreenshot",
|
||||
);
|
||||
expect(screenshots.map((artifact) => artifact.source).sort()).toEqual(
|
||||
[incomplete, complete].map((directory) => path.join(directory, "web-ui-chat.png")).sort(),
|
||||
);
|
||||
const comment = renderEvidenceComment({
|
||||
manifest,
|
||||
marker: "<!-- synthetic-mantis-proof -->",
|
||||
rawBase: "https://artifacts.example.test/current",
|
||||
});
|
||||
for (const [directory, marker] of [
|
||||
[incomplete, "incomplete"],
|
||||
[complete, "complete"],
|
||||
] as const) {
|
||||
const relative = path.basename(directory);
|
||||
for (const [file, content] of [
|
||||
["web-ui-chat.png", "screenshot"],
|
||||
["web-ui-chat.webm", "video"],
|
||||
]) {
|
||||
const artifact = manifest.artifacts.find((entry) => entry.path === `${relative}/${file}`);
|
||||
expect(artifact?.targetPath).toBe(`${relative}/${file}`);
|
||||
expect(readFileSync(artifact!.source, "utf8")).toBe(`${marker} ${content}`);
|
||||
expect(comment).toContain(`https://artifacts.example.test/current/${relative}/${file}`);
|
||||
expect(readFileSync(result.reportPath, "utf8")).toContain(`${relative}/${file}`);
|
||||
}
|
||||
}
|
||||
expect(readFileSync(prior.reportPath, "utf8")).toBe(priorReport);
|
||||
expect(readFileSync(prior.manifestPath, "utf8")).toBe(priorManifest);
|
||||
expect(readFileSync(path.join(priorCapture, "web-ui-chat.png"), "utf8")).toBe(
|
||||
"prior screenshot",
|
||||
);
|
||||
expect(readFileSync(result.reportPath, "utf8")).not.toContain(path.basename(priorCapture));
|
||||
});
|
||||
});
|
||||
|
||||
it("retains setup failures before a capture exists as publishable failure evidence", () => {
|
||||
withTempDir((dir) => {
|
||||
writeFileSync(path.join(dir, "setup.log"), "candidate installation failed");
|
||||
const result = writeWebUiChatEvidence(["--output-dir", dir, "--status", "fail"]);
|
||||
const manifest = loadEvidenceManifest(result.manifestPath);
|
||||
expect(manifest.comparison).toMatchObject({
|
||||
candidate: { expectationMet: false, status: "fail" },
|
||||
pass: false,
|
||||
});
|
||||
expect(manifest.artifacts.map((artifact) => artifact.targetPath)).toEqual([
|
||||
"setup.log",
|
||||
"mantis-report.md",
|
||||
"mantis-evidence.json",
|
||||
]);
|
||||
expect(manifest.summary).toContain("could not complete");
|
||||
expect(readFileSync(result.reportPath, "utf8")).toContain("`setup.log` (present)");
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps a failed process verdict even when an attempt completed its captures", () => {
|
||||
withTempDir((dir) => {
|
||||
captureAttempt(dir, "completed before process failure");
|
||||
const result = writeWebUiChatEvidence(["--output-dir", dir, "--status", "fail"]);
|
||||
expect(loadEvidenceManifest(result.manifestPath).comparison.pass).toBe(false);
|
||||
expect(readFileSync(result.reportPath, "utf8")).toContain("Status: fail");
|
||||
});
|
||||
});
|
||||
|
||||
it("uses the explicit pass status and includes report output", () => {
|
||||
withTempDir((dir) => {
|
||||
writeFileSync(path.join(dir, "web-ui-chat.png"), "png");
|
||||
writeFileSync(
|
||||
path.join(dir, "web-ui-chat-proof.json"),
|
||||
`${JSON.stringify({ status: "pass" })}\n`,
|
||||
);
|
||||
|
||||
const result = writeWebUiChatEvidence([
|
||||
"--output-dir",
|
||||
dir,
|
||||
"--candidate-ref",
|
||||
"main",
|
||||
"--status",
|
||||
"pass",
|
||||
]);
|
||||
|
||||
expect(result.manifest.comparison).toMatchObject({
|
||||
candidate: { fixed: true, ref: "main", status: "pass" },
|
||||
pass: true,
|
||||
it.each(["absent", "old flat files", "split attempts"])(
|
||||
"rejects a passing verdict with %s instead of using unrelated proof",
|
||||
(kind) => {
|
||||
withTempDir((dir) => {
|
||||
const metadata = JSON.stringify({ status: "pass" });
|
||||
if (kind === "old flat files") {
|
||||
writeFileSync(path.join(dir, "web-ui-chat.png"), "old screenshot");
|
||||
writeFileSync(path.join(dir, "web-ui-chat-proof.json"), metadata);
|
||||
} else if (kind === "split attempts") {
|
||||
captureAttempt(dir, "screenshot only", false);
|
||||
const metadataOnly = mkdtempSync(path.join(dir, "mantis-chat-proof-"));
|
||||
writeFileSync(path.join(metadataOnly, "web-ui-chat-proof.json"), metadata);
|
||||
}
|
||||
expect(() => writeWebUiChatEvidence(["--output-dir", dir, "--status", "pass"])).toThrow(
|
||||
"Passing proof requires a captured screenshot and proof metadata",
|
||||
);
|
||||
expect(existsSync(path.join(dir, "mantis-evidence.json"))).toBe(false);
|
||||
});
|
||||
expect(readFileSync(result.reportPath, "utf8")).toContain(
|
||||
"Screenshot: `web-ui-chat.png` (present)",
|
||||
},
|
||||
);
|
||||
|
||||
it("refuses to rewrite reports when the builder is replayed against the same invocation", () => {
|
||||
withTempDir((dir) => {
|
||||
captureAttempt(dir, "original");
|
||||
const original = writeWebUiChatEvidence(["--output-dir", dir, "--status", "pass"]);
|
||||
const report = readFileSync(original.reportPath, "utf8");
|
||||
const manifest = readFileSync(original.manifestPath, "utf8");
|
||||
const retry = captureAttempt(dir, "retained retry", false);
|
||||
expect(() => writeWebUiChatEvidence(["--output-dir", dir, "--status", "fail"])).toThrow(
|
||||
"use a fresh invocation directory",
|
||||
);
|
||||
expect(readFileSync(original.reportPath, "utf8")).toBe(report);
|
||||
expect(readFileSync(original.manifestPath, "utf8")).toBe(manifest);
|
||||
expect(readFileSync(path.join(retry, "web-ui-chat.png"), "utf8")).toBe(
|
||||
"retained retry screenshot",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ const WORKFLOW = ".github/workflows/mantis-web-ui-chat-proof.yml";
|
|||
const SHARED_RESOLVE_WORKFLOW = ".github/workflows/mantis-resolve-request.yml";
|
||||
|
||||
type WorkflowStep = {
|
||||
id?: string;
|
||||
if?: string;
|
||||
env?: Record<string, string>;
|
||||
name?: string;
|
||||
run?: string;
|
||||
uses?: string;
|
||||
|
|
@ -14,6 +17,7 @@ type WorkflowStep = {
|
|||
};
|
||||
|
||||
type WorkflowJob = {
|
||||
outputs?: Record<string, string>;
|
||||
permissions?: Record<string, string>;
|
||||
steps?: WorkflowStep[];
|
||||
};
|
||||
|
|
@ -91,6 +95,37 @@ describe("Mantis Web UI chat proof workflow", () => {
|
|||
expect(publish?.run).not.toContain("--import tsx");
|
||||
});
|
||||
|
||||
it("retains one invocation root through capture, failure reporting, and artifact upload", () => {
|
||||
const job = workflowJob("run_web_ui_chat");
|
||||
const allocate = job.steps?.find((step) => step.id === "prepare_evidence");
|
||||
const capture = job.steps?.find((step) => step.id === "run_mantis");
|
||||
const report = job.steps?.find((step) => step.id === "build_evidence");
|
||||
const upload = job.steps?.find((step) => step.name === "Upload Mantis web UI chat artifacts");
|
||||
const rootOutput = "${{ steps.prepare_evidence.outputs.output_dir }}";
|
||||
|
||||
expect(allocate?.run).toContain('mktemp -d "$output_parent/run.XXXXXX"');
|
||||
expect(capture?.run).toContain(`root="${rootOutput}"`);
|
||||
expect(capture?.run).toContain('OPENCLAW_MANTIS_WEB_UI_CHAT_OUTPUT_DIR="$root"');
|
||||
expect(capture?.run).toContain('OPENCLAW_UI_E2E_ARTIFACT_DIR="$root"');
|
||||
// Older candidates need the allocator imported by both overlaid harness files.
|
||||
expect(capture?.run).toContain(
|
||||
'"${GITHUB_WORKSPACE}/ui/src/test-helpers/control-ui-e2e-artifacts.ts"',
|
||||
);
|
||||
expect(capture?.run).toContain(
|
||||
'"$candidate_repo/ui/src/test-helpers/control-ui-e2e-artifacts.ts"',
|
||||
);
|
||||
expect(report?.if).toContain("always()");
|
||||
expect(report?.run).toContain(`root="${rootOutput}"`);
|
||||
expect(report?.run).toContain('--output-dir "$root"');
|
||||
expect(report?.env?.PROOF_STATUS).toBe(
|
||||
"${{ steps.run_mantis.outcome == 'success' && 'pass' || 'fail' }}",
|
||||
);
|
||||
expect(upload?.if).toContain("always()");
|
||||
expect(upload?.with?.path).toBe(rootOutput);
|
||||
expect(job.outputs?.output_dir).toBe(rootOutput);
|
||||
expect(upload?.with?.name).toBe(job.outputs?.artifact_name);
|
||||
});
|
||||
|
||||
it("only treats explicit candidate assignments as PR head overrides", () => {
|
||||
const pattern = candidateOverridePattern();
|
||||
|
||||
|
|
|
|||
|
|
@ -6606,10 +6606,16 @@ printf '%s\\n' "$DEEPSEEK_API_KEY" "$DEEPINFRA_API_KEY"`,
|
|||
|
||||
for (const [workflowPath, jobName, count] of cases) {
|
||||
const job = workflowJob(workflowPath, jobName);
|
||||
const initialSteps = [
|
||||
"Checkout harness ref",
|
||||
...(jobName === "run_web_ui_chat" ? ["Allocate invocation evidence directory"] : []),
|
||||
"Prepare Git owner",
|
||||
"Setup Node environment",
|
||||
];
|
||||
expect(
|
||||
job.steps?.slice(0, 3).map(({ name }) => name),
|
||||
job.steps?.slice(0, initialSteps.length).map(({ name }) => name),
|
||||
workflowPath,
|
||||
).toEqual(["Checkout harness ref", "Prepare Git owner", "Setup Node environment"]);
|
||||
).toEqual(initialSteps);
|
||||
expect(job.steps?.filter(({ name }) => name === "Prepare Git owner")).toEqual([
|
||||
{
|
||||
name: "Prepare Git owner",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Control UI E2E tests cover Codex final-answer candidate Activity rendering.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -14,18 +14,17 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"codex-answer-candidates",
|
||||
);
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("codex-answer-candidates");
|
||||
}
|
||||
});
|
||||
|
||||
async function screenshot(page: Page, name: string) {
|
||||
if (!captureUiProof) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
@ -35,9 +34,6 @@ async function screenshot(page: Page, name: string) {
|
|||
|
||||
suite.define(() => {
|
||||
it("shows superseded and authoritative selected answers only in Activity", async () => {
|
||||
if (captureUiProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
await suite.withPage(
|
||||
{
|
||||
locale: "en-US",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type BrowserContext, type Locator, type Page } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { AuditRunInspectResult } from "../../../packages/gateway-protocol/src/schema/audit-run.js";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
|
|
@ -21,7 +22,12 @@ const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
|||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.resolve(".artifacts/control-ui-e2e/activity-run-inspector-r6");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("activity-run-inspector-r6");
|
||||
}
|
||||
});
|
||||
|
||||
let browser: Browser;
|
||||
let server: ControlUiE2eServer;
|
||||
|
|
@ -91,7 +97,6 @@ async function screenshot(page: Page, name: string) {
|
|||
if (!captureUiProof) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
controlUiBundledGatewayUrl,
|
||||
controlUiSessionUrl,
|
||||
|
|
@ -15,7 +15,10 @@ const suite = createControlUiE2eSuite({
|
|||
startServerBeforeBrowser: true,
|
||||
});
|
||||
|
||||
const outputDir = path.resolve(process.cwd(), ".artifacts/control-ui-e2e/session-activity-feed");
|
||||
let outputDir: string;
|
||||
beforeEach(() => {
|
||||
outputDir = createControlUiE2eArtifactDir("session-activity-feed");
|
||||
});
|
||||
const proofPhase = process.env.OPENCLAW_MENU_THEME_PROOF_PHASE;
|
||||
|
||||
suite.define(() => {
|
||||
|
|
@ -295,7 +298,6 @@ suite.define(() => {
|
|||
const onlineToggle = page.getByRole("button", { name: "Online", exact: true });
|
||||
await expect.poll(() => onlineToggle.getAttribute("aria-expanded")).toBe("true");
|
||||
await expect.poll(() => page.locator(".sidebar-online__person").count()).toBe(4);
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
await page.locator(".sidebar").screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(outputDir, "01-sidebar-online-default-open-light.png"),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -11,7 +12,12 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.resolve(".artifacts/control-ui-e2e/control-ui-activity-summaries");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("control-ui-activity-summaries");
|
||||
}
|
||||
});
|
||||
|
||||
suite.define(() => {
|
||||
it("updates one visible tool summary from running to completed output", async () => {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Control UI E2E tests cover Agents channel runtime-status precedence.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -14,13 +14,17 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "channel-runtime-status");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("channel-runtime-status");
|
||||
}
|
||||
});
|
||||
|
||||
async function screenshot(page: Page) {
|
||||
if (!captureUiProof) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Control UI E2E proves per-agent config writes use the canonical keyed shape.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -13,7 +13,12 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "agent-config-save");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("agent-config-save");
|
||||
}
|
||||
});
|
||||
|
||||
const requireRecord = createRequireRecord("record", "expected-object-value");
|
||||
|
||||
|
|
@ -52,7 +57,6 @@ suite.define(() => {
|
|||
const reload = agentsPage.getByRole("button", { name: "Reload Config" });
|
||||
await reload.waitFor();
|
||||
if (captureUiProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
@ -194,7 +198,6 @@ suite.define(() => {
|
|||
await page.getByRole("alert").filter({ hasText: "mock validation failure" }).waitFor();
|
||||
|
||||
if (captureUiProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Control UI E2E proves the complete Agents -> Tools GitHub authorization presentation.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -13,18 +13,17 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"agent-github-device-authorization",
|
||||
);
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("agent-github-device-authorization");
|
||||
}
|
||||
});
|
||||
|
||||
async function capture(page: Page, name: string) {
|
||||
if (!captureUiProof) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Real browser proof that agent model fallbacks follow the Gateway's ownership contract.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -15,7 +15,12 @@ const primaryModel = "openai/gpt-5.4";
|
|||
const inheritedFallback = "anthropic/claude-sonnet-4-6";
|
||||
const writerWorkspace = "/tmp/agents/writer";
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.resolve(".artifacts/control-ui-e2e/agent-context-ownership");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("agent-context-ownership");
|
||||
}
|
||||
});
|
||||
|
||||
suite.define(() => {
|
||||
it.each([
|
||||
|
|
@ -120,7 +125,6 @@ suite.define(() => {
|
|||
.toBe("/settings/agents/writer/overview");
|
||||
|
||||
if (captureUiProof && model && !("primary" in Object(model))) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Control UI E2E tests cover chip-selected page scope and the all-agents escape.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway, type MockGatewayControls } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -14,7 +14,12 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "agent-page-scope");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("agent-page-scope");
|
||||
}
|
||||
});
|
||||
|
||||
function requestParams(request: { params?: unknown }): Record<string, unknown> {
|
||||
return request.params && typeof request.params === "object"
|
||||
|
|
@ -38,7 +43,6 @@ async function screenshot(page: Page, name: string) {
|
|||
if (!captureUiProof) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
@ -205,9 +209,6 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("keeps a refreshed canonical roster while chat startup remains delayed", async () => {
|
||||
if (captureUiProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
await suite.withPage(
|
||||
{
|
||||
locale: "en-US",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Control UI tests cover bounded authenticated agent-picker avatar fetches.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -14,18 +14,17 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"agent-select-avatar-timeout",
|
||||
);
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("agent-select-avatar-timeout");
|
||||
}
|
||||
});
|
||||
|
||||
async function screenshot(page: Page, name: string) {
|
||||
if (!captureUiProof) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { BrowserContext, Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
controlUiBundledGatewayUrl,
|
||||
controlUiBundledSettingsStorageKey,
|
||||
|
|
@ -20,12 +20,12 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"agent-selection-persistence",
|
||||
);
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("agent-selection-persistence");
|
||||
}
|
||||
});
|
||||
|
||||
const agentsList = {
|
||||
agents: [
|
||||
|
|
@ -78,7 +78,6 @@ async function screenshot(page: Page, name: string) {
|
|||
if (!captureUiProof) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.waitForTimeout(600);
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
|
|
@ -139,9 +138,6 @@ async function openPage(
|
|||
|
||||
suite.define(() => {
|
||||
it("restores the selected agent when a fresh shell reopens the canonical global session", async () => {
|
||||
if (captureUiProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Control UI tests cover the canonical scrollbar profile and native-style text selection.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Locator, Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
installMockGateway,
|
||||
waitForControlUiSettingsTakeover,
|
||||
|
|
@ -17,12 +17,12 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const uiProofArtifactDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"app-chrome-interaction",
|
||||
);
|
||||
let uiProofArtifactDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProofEnabled) {
|
||||
uiProofArtifactDir = createControlUiE2eArtifactDir("app-chrome-interaction");
|
||||
}
|
||||
});
|
||||
|
||||
async function dragAcross(page: Page, locator: Locator): Promise<string> {
|
||||
await locator.scrollIntoViewIfNeeded();
|
||||
|
|
@ -52,7 +52,6 @@ async function captureUiProof(page: Page, fileName: string) {
|
|||
if (!captureUiProofEnabled) {
|
||||
return;
|
||||
}
|
||||
await mkdir(uiProofArtifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(uiProofArtifactDir, fileName),
|
||||
|
|
@ -61,9 +60,6 @@ async function captureUiProof(page: Page, fileName: string) {
|
|||
|
||||
suite.define(() => {
|
||||
it("keeps canonical scrollbars without horizontal model-picker overflow and preserves selection", async () => {
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(uiProofArtifactDir, { recursive: true });
|
||||
}
|
||||
await suite.withPage(
|
||||
{
|
||||
locale: "en-US",
|
||||
|
|
@ -258,9 +254,6 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("resolves the scrollbar thumb from theme tokens and captures dark/light proof", async () => {
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(uiProofArtifactDir, { recursive: true });
|
||||
}
|
||||
const thumbColorByScheme: Record<"dark" | "light", string> = { dark: "", light: "" };
|
||||
for (const colorScheme of ["dark", "light"] as const) {
|
||||
await suite.withPage(
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Control UI tests cover Appearance override provenance and restoring product defaults.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Locator, Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { importCustomThemeFromUrl } from "../pages/config/custom-theme-import.ts";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
controlUiBundledGatewayUrl,
|
||||
controlUiBundledSettingsStorageKey,
|
||||
|
|
@ -24,12 +24,12 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const uiProofArtifactDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"appearance-settings-defaults",
|
||||
);
|
||||
let uiProofArtifactDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProofEnabled) {
|
||||
uiProofArtifactDir = createControlUiE2eArtifactDir("appearance-settings-defaults");
|
||||
}
|
||||
});
|
||||
function settingsStorageKey(): string {
|
||||
return controlUiBundledSettingsStorageKey(suite.server.baseUrl);
|
||||
}
|
||||
|
|
@ -169,7 +169,6 @@ async function captureViewport(page: Page, filename: string): Promise<void> {
|
|||
if (!captureUiProofEnabled) {
|
||||
return;
|
||||
}
|
||||
await mkdir(uiProofArtifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(uiProofArtifactDir, filename),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Control UI E2E tests cover approval queue behavior through the Gateway WebSocket.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import type { Page } from "playwright";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { controlUiSessionUrl, installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -15,7 +15,12 @@ const suite = createControlUiE2eSuite({
|
|||
let page: Page | undefined;
|
||||
const activeSessionKey = "agent:main:main";
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "approval-flow");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("approval-flow");
|
||||
}
|
||||
});
|
||||
|
||||
function approval(id: string, command: string, createdAtMs: number, sessionKey = activeSessionKey) {
|
||||
return {
|
||||
|
|
@ -95,9 +100,6 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("keeps approvals passive until the Inbox opens the full queue", async () => {
|
||||
if (captureUiProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
const context = await suite.browser.newContext({
|
||||
viewport: { height: 800, width: 1200 },
|
||||
...(captureUiProof
|
||||
|
|
@ -144,9 +146,6 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("keeps no-auth inline and Inbox approvals readable while blocking decisions", async () => {
|
||||
if (captureUiProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
const context = await suite.browser.newContext({ viewport: { height: 800, width: 1200 } });
|
||||
const currentPage = await context.newPage();
|
||||
page = currentPage;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { copyFile, mkdir, rm } from "node:fs/promises";
|
||||
import { copyFile, rm } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Browser, BrowserContext, Page, Video } from "playwright";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { afterEach, beforeEach, expect, it } from "vitest";
|
||||
import type {
|
||||
AllowedApprovalSnapshot,
|
||||
PendingApprovalSnapshot,
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
controlUiE2eWaitTimeoutMs,
|
||||
installMockGateway,
|
||||
|
|
@ -21,9 +22,8 @@ const suite = createControlUiE2eSuite({
|
|||
|
||||
const APPROVAL_ID = "Approval:Mobile/東京 100% 🦞";
|
||||
const APPROVAL_NOW_MS = Date.UTC(2026, 6, 10, 18, 0, 0);
|
||||
const ARTIFACT_DIR = path.resolve(".artifacts/control-ui-e2e/approval-page");
|
||||
let ARTIFACT_DIR: string;
|
||||
const CAPTURE_UI_PROOF = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const MOBILE_RAW_VIDEO_DIR = path.join(ARTIFACT_DIR, "mobile-raw");
|
||||
const MOBILE_VIEWPORT = { height: 844, width: 390 } as const;
|
||||
const DESKTOP_VIEWPORT = { height: 800, width: 1200 } as const;
|
||||
|
||||
|
|
@ -36,7 +36,6 @@ type ApprovalSurface = {
|
|||
};
|
||||
|
||||
const openContexts = new Set<BrowserContext>();
|
||||
const rawVideoDirs = new Set<string>();
|
||||
|
||||
function approvalPath(basePath: string): string {
|
||||
return `${basePath}/approve/${encodeURIComponent(APPROVAL_ID)}`;
|
||||
|
|
@ -116,13 +115,10 @@ async function createSurface(params: {
|
|||
recordVideo?: boolean;
|
||||
viewport: { height: number; width: number };
|
||||
}): Promise<ApprovalSurface> {
|
||||
const rawVideoDir = params.recordVideo && CAPTURE_UI_PROOF ? MOBILE_RAW_VIDEO_DIR : undefined;
|
||||
if (rawVideoDir) {
|
||||
await mkdir(ARTIFACT_DIR, { recursive: true });
|
||||
await rm(rawVideoDir, { force: true, recursive: true });
|
||||
await mkdir(rawVideoDir, { recursive: true });
|
||||
rawVideoDirs.add(rawVideoDir);
|
||||
}
|
||||
const rawVideoDir =
|
||||
params.recordVideo && CAPTURE_UI_PROOF
|
||||
? createControlUiE2eArtifactDir("mobile-raw", ARTIFACT_DIR)
|
||||
: undefined;
|
||||
const context = await requireBrowser().newContext({
|
||||
colorScheme: "dark",
|
||||
hasTouch: params.viewport.width <= MOBILE_VIEWPORT.width,
|
||||
|
|
@ -158,7 +154,7 @@ async function closeRecordedSurface(surface: ApprovalSurface, targetName: string
|
|||
}
|
||||
const rawVideoPath = await video.path();
|
||||
await copyFile(rawVideoPath, path.join(ARTIFACT_DIR, targetName));
|
||||
rawVideoDirs.delete(surface.rawVideoDir);
|
||||
// Remove raw footage only after the retained named copy succeeds.
|
||||
await rm(surface.rawVideoDir, { force: true, recursive: true });
|
||||
}
|
||||
|
||||
|
|
@ -209,7 +205,6 @@ async function captureProof(page: Page, name: string): Promise<void> {
|
|||
if (!CAPTURE_UI_PROOF) {
|
||||
return;
|
||||
}
|
||||
await mkdir(ARTIFACT_DIR, { recursive: true });
|
||||
await waitForStableApprovalPaint(page);
|
||||
await page.screenshot({ path: path.join(ARTIFACT_DIR, name) });
|
||||
}
|
||||
|
|
@ -218,7 +213,6 @@ async function captureDocumentProof(page: Page, name: string): Promise<void> {
|
|||
if (!CAPTURE_UI_PROOF) {
|
||||
return;
|
||||
}
|
||||
await mkdir(ARTIFACT_DIR, { recursive: true });
|
||||
await page.evaluate(async () => {
|
||||
await document.fonts.ready;
|
||||
await new Promise<void>((resolve) => {
|
||||
|
|
@ -287,13 +281,14 @@ async function expectMobilePendingLayout(page: Page): Promise<void> {
|
|||
}
|
||||
|
||||
suite.define(() => {
|
||||
beforeEach(() => {
|
||||
if (CAPTURE_UI_PROOF) {
|
||||
ARTIFACT_DIR = createControlUiE2eArtifactDir("approval-page");
|
||||
}
|
||||
});
|
||||
afterEach(async () => {
|
||||
await Promise.all([...openContexts].map((context) => context.close().catch(() => {})));
|
||||
openContexts.clear();
|
||||
await Promise.all(
|
||||
[...rawVideoDirs].map((rawVideoDir) => rm(rawVideoDir, { force: true, recursive: true })),
|
||||
);
|
||||
rawVideoDirs.clear();
|
||||
});
|
||||
|
||||
it("keeps one canonical winner across conflicting surfaces and terminal reload", async () => {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Control UI E2E: grapheme-aware avatar initials remain intact across every
|
||||
// live agent-avatar fallback surface.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -15,7 +15,12 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "avatar-initial-emoji");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("avatar-initial-emoji");
|
||||
}
|
||||
});
|
||||
|
||||
const emojiAgent = { id: "emoji", identity: { name: "🚀Rocket" }, name: "🚀Rocket" };
|
||||
const asciiAgent = { id: "main", identity: { name: "Main" }, name: "Main" };
|
||||
|
|
@ -43,7 +48,6 @@ async function screenshot(page: Page, name: string) {
|
|||
if (!captureUiProof) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { buildWidgetDocument } from "../../../src/canvas/wrap.js";
|
|||
import { buildBoardWidgetSandboxPath } from "../../../src/gateway/board-sandbox.js";
|
||||
import { createSandboxHostHttpServer } from "../../../src/gateway/mcp-app-sandbox-http.js";
|
||||
import { getGatewayE2ePortBlock } from "../../../src/gateway/test-helpers.e2e.js";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
controlUiBundledSettingsStorageKey,
|
||||
|
|
@ -266,8 +267,7 @@ describeControlUiE2e("Control UI dashboard A2UI", () => {
|
|||
expect(scrollbar.ratio).toBeLessThan(0.2);
|
||||
if (scrollbarProofLabel) {
|
||||
const screenshotPath = path.resolve(
|
||||
process.cwd(),
|
||||
".artifacts/control-ui-e2e/widget-scrollbar",
|
||||
createControlUiE2eArtifactDir("widget-scrollbar"),
|
||||
`${scrollbarProofLabel}-${colorScheme}.png`,
|
||||
);
|
||||
await mkdir(path.dirname(screenshotPath), { recursive: true });
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// Regression: the chat transcript must repaint after a dashboard -> split face
|
||||
// switch re-stamps it into the sidebar region (issue: virtualizer stayed
|
||||
// detached until an unrelated re-render, painting a blank pane).
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { beforeEach, afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
controlUiBundledSettingsStorageKey,
|
||||
|
|
@ -21,7 +21,12 @@ const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
|||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const sessionKey = "agent:main:board-split-transcript";
|
||||
const proofDir = path.resolve(".artifacts/control-ui-e2e/dashboard-side-chat-tabs");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (process.env.OPENCLAW_UI_E2E_RECORD === "1") {
|
||||
proofDir = createControlUiE2eArtifactDir("dashboard-side-chat-tabs");
|
||||
}
|
||||
});
|
||||
|
||||
let browser: Browser;
|
||||
let controlUi: ControlUiE2eServer;
|
||||
|
|
@ -270,9 +275,6 @@ describeControlUiE2e("Board split transcript restore", () => {
|
|||
|
||||
it("transitions between Dashboard and Split with the whole side panel", async () => {
|
||||
const recordProof = process.env.OPENCLAW_UI_E2E_RECORD === "1";
|
||||
if (recordProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1400, height: 900 },
|
||||
...(recordProof
|
||||
|
|
@ -380,9 +382,6 @@ describeControlUiE2e("Board split transcript restore", () => {
|
|||
|
||||
it("activates Side chat from a split dashboard panel", async () => {
|
||||
const recordProof = process.env.OPENCLAW_UI_E2E_RECORD === "1";
|
||||
if (recordProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1400, height: 900 },
|
||||
...(recordProof
|
||||
|
|
@ -445,9 +444,6 @@ describeControlUiE2e("Board split transcript restore", () => {
|
|||
|
||||
it("does not offer Discussion when the gateway has no discussion provider", async () => {
|
||||
const recordProof = process.env.OPENCLAW_UI_E2E_RECORD === "1";
|
||||
if (recordProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1400, height: 900 },
|
||||
...(recordProof
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import path from "node:path";
|
||||
// Control UI E2E tests cover visible browser dictation state through a real composer.
|
||||
import { expect, it } from "vitest";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
|
|
@ -69,7 +70,11 @@ suite.define(() => {
|
|||
const committedDraft = cancel ? "ship it" : expected;
|
||||
expect(await textarea.inputValue()).toBe(committedDraft);
|
||||
expect(await gateway.getRequests("chat.send")).toHaveLength(0);
|
||||
await captureComposerProof(page, `dictation-${name.replaceAll(" ", "-")}-committed.png`);
|
||||
await captureComposerProof(
|
||||
suite,
|
||||
page,
|
||||
`dictation-${name.replaceAll(" ", "-")}-committed.png`,
|
||||
);
|
||||
expect(await textarea.inputValue()).toBe(committedDraft);
|
||||
});
|
||||
});
|
||||
|
|
@ -132,7 +137,7 @@ suite.define(() => {
|
|||
await expect
|
||||
.poll(() => textarea.inputValue())
|
||||
.toBe("keep this draft discard this speech too");
|
||||
await captureComposerProof(page, "dictation-new-session-recognized-preview.png");
|
||||
await captureComposerProof(suite, page, "dictation-new-session-recognized-preview.png");
|
||||
await page.keyboard.press("Escape");
|
||||
await expect.poll(() => textarea.inputValue()).toBe("keep this draft");
|
||||
await gateway.waitForRequest("talk.session.close");
|
||||
|
|
@ -140,7 +145,7 @@ suite.define(() => {
|
|||
true,
|
||||
);
|
||||
expect(await gateway.getRequests("sessions.create")).toHaveLength(0);
|
||||
await captureComposerProof(page, "dictation-new-session-cancelled.png");
|
||||
await captureComposerProof(suite, page, "dictation-new-session-cancelled.png");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -173,10 +178,10 @@ suite.define(() => {
|
|||
|
||||
await expect.poll(() => toggle.getAttribute("aria-checked")).toBe("false");
|
||||
await expect.poll(() => picker.getAttribute("open")).not.toBeNull();
|
||||
await captureComposerProof(page, "microphone-picker-hold-toggle.png");
|
||||
await captureComposerProof(suite, page, "microphone-picker-hold-toggle.png");
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
path: ".artifacts/control-ui-e2e/voice-controls/microphone-picker-hold-toggle-full.png",
|
||||
path: path.join(suite.artifactDir, "voice-controls/microphone-picker-hold-toggle-full.png"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -204,10 +209,13 @@ suite.define(() => {
|
|||
await expect
|
||||
.poll(() => unavailable.getByRole("button", { name: "Configure" }).count())
|
||||
.toBe(2);
|
||||
await captureComposerProof(page, "microphone-picker-capability-gating.png");
|
||||
await captureComposerProof(suite, page, "microphone-picker-capability-gating.png");
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
path: ".artifacts/control-ui-e2e/voice-controls/microphone-picker-capability-gating-full.png",
|
||||
path: path.join(
|
||||
suite.artifactDir,
|
||||
"voice-controls/microphone-picker-capability-gating-full.png",
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -252,10 +260,10 @@ suite.define(() => {
|
|||
expect(await composer.locator(".agent-chat__dictation-elapsed").count()).toBe(0);
|
||||
await expect.poll(() => stop.isVisible()).toBe(true);
|
||||
await expect.poll(() => send.isVisible()).toBe(true);
|
||||
await captureComposerProof(page, "dictation-status-actions.png");
|
||||
await captureComposerProof(suite, page, "dictation-status-actions.png");
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
path: ".artifacts/control-ui-e2e/voice-controls/dictation-latched-after-release.png",
|
||||
path: path.join(suite.artifactDir, "voice-controls/dictation-latched-after-release.png"),
|
||||
});
|
||||
const composerBox = await composer.boundingBox();
|
||||
expect(composerBox).not.toBeNull();
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { openChatSidePanelType } from "./chat-side-panel.test-support.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
|
@ -12,10 +13,12 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.resolve(
|
||||
process.cwd(),
|
||||
".artifacts/control-ui-e2e/browser-screenshot-body-cancel",
|
||||
);
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProofEnabled) {
|
||||
proofDir = createControlUiE2eArtifactDir("browser-screenshot-body-cancel");
|
||||
}
|
||||
});
|
||||
|
||||
suite.define(() => {
|
||||
it("keeps the status error visible and cancels the unread media body", async () => {
|
||||
|
|
@ -175,7 +178,6 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(proofDir, "failed-screenshot.png"),
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ suite.define(() => {
|
|||
)
|
||||
.toBe(true);
|
||||
expect(await gateway.getRequests("talk.client.create")).toHaveLength(0);
|
||||
await captureMicrophoneLossProof(page, "prepared-input-pending.png");
|
||||
await captureMicrophoneLossProof(suite, page, "prepared-input-pending.png");
|
||||
const guidance = page.locator('.agent-chat__talk-status[role="status"]');
|
||||
await expect
|
||||
.poll(() => guidance.allTextContents())
|
||||
|
|
@ -76,7 +76,7 @@ suite.define(() => {
|
|||
.toBe(1);
|
||||
await expect.poll(() => guidance.count()).toBe(0);
|
||||
expect(await gateway.getRequests("talk.client.create")).toHaveLength(1);
|
||||
await captureMicrophoneLossProof(page, "prepared-input-ready.png");
|
||||
await captureMicrophoneLossProof(suite, page, "prepared-input-ready.png");
|
||||
await page.getByRole("button", { name: "Stop voice input" }).click();
|
||||
});
|
||||
});
|
||||
|
|
@ -120,10 +120,10 @@ suite.define(() => {
|
|||
)
|
||||
.toMatchObject({ status: "listening" });
|
||||
} catch (error) {
|
||||
await captureMicrophoneLossProof(page, "microphone-loss-setup-failure.png");
|
||||
await captureMicrophoneLossProof(suite, page, "microphone-loss-setup-failure.png");
|
||||
throw error;
|
||||
}
|
||||
await captureMicrophoneLossProof(page, "microphone-loss-before-listening.png");
|
||||
await captureMicrophoneLossProof(suite, page, "microphone-loss-before-listening.png");
|
||||
|
||||
await page.evaluate(() => {
|
||||
(
|
||||
|
|
@ -150,7 +150,7 @@ suite.define(() => {
|
|||
}),
|
||||
)
|
||||
.toEqual({ tracksStopped: 1, peerClosed: true, trackState: "ended", audioElements: 0 });
|
||||
await captureMicrophoneLossProof(page, "microphone-loss-after-error.png");
|
||||
await captureMicrophoneLossProof(suite, page, "microphone-loss-after-error.png");
|
||||
await gateway.waitForRequest("talk.client.close");
|
||||
await page.getByRole("button", { name: "Dismiss voice input error" }).click();
|
||||
console.info(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import {
|
||||
dispatchOpenAiTalkEvent,
|
||||
|
|
@ -18,8 +18,7 @@ const suite = createControlUiE2eSuite({
|
|||
|
||||
suite.define(() => {
|
||||
it("renders and saves overlapping replies beside their delayed user transcripts", async () => {
|
||||
const artifactDir = path.join(process.cwd(), ".artifacts/control-ui-e2e/transcript-ordering");
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
const artifactDir = createControlUiE2eArtifactDir("transcript-ordering");
|
||||
await suite.withPage(
|
||||
{
|
||||
permissions: ["microphone"],
|
||||
|
|
|
|||
|
|
@ -258,7 +258,7 @@ suite.define(() => {
|
|||
Number(await page.locator(".agent-chat__voice-activity").getAttribute("data-level")),
|
||||
)
|
||||
.toBeGreaterThan(0);
|
||||
await captureComposerProof(page, "01-voice-live-listening.png");
|
||||
await captureComposerProof(suite, page, "01-voice-live-listening.png");
|
||||
|
||||
// Enter-sends while voice is active; the deferred chat.send keeps the
|
||||
// run abortable so both stop controls render side by side.
|
||||
|
|
@ -296,16 +296,16 @@ suite.define(() => {
|
|||
).toBe(false);
|
||||
expect(await stopVoice.locator(".agent-chat__voice-activity").count()).toBe(1);
|
||||
expect(await page.locator(".chat-send-btn--stop").count()).toBe(1);
|
||||
await captureComposerProof(page, "02-voice-plus-run-stop.png");
|
||||
await captureComposerProof(suite, page, "02-voice-plus-run-stop.png");
|
||||
|
||||
await page.emulateMedia({ colorScheme: "dark" });
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => document.documentElement.dataset.themeMode))
|
||||
.toBe("dark");
|
||||
await captureComposerProof(page, "03-voice-plus-run-stop-dark.png");
|
||||
await captureComposerProof(suite, page, "03-voice-plus-run-stop-dark.png");
|
||||
|
||||
await stopVoice.hover();
|
||||
await captureComposerProof(page, "04-voice-live-hover-stop-glyph.png");
|
||||
await captureComposerProof(suite, page, "04-voice-live-hover-stop-glyph.png");
|
||||
|
||||
// Stopping voice must leave the run (and its stop control) untouched.
|
||||
await stopVoice.click();
|
||||
|
|
@ -333,7 +333,7 @@ suite.define(() => {
|
|||
|
||||
await page.setViewportSize({ width: 1366, height: 900 });
|
||||
await page.goto(`${suite.server.baseUrl}chat`);
|
||||
await captureVideoTalkProof(page, "01-before-video-talk.png");
|
||||
await captureVideoTalkProof(suite, page, "01-before-video-talk.png");
|
||||
|
||||
await page.getByRole("button", { name: "Start voice input" }).click();
|
||||
const request = await gateway.waitForRequest("talk.client.create");
|
||||
|
|
@ -382,7 +382,7 @@ suite.define(() => {
|
|||
item_id: "unintelligible-input",
|
||||
error: { message: "The audio could not be transcribed." },
|
||||
});
|
||||
await captureMicrophoneLossProof(page, "input-transcription-error.png");
|
||||
await captureMicrophoneLossProof(suite, page, "input-transcription-error.png");
|
||||
const transcriptionError = page.getByRole("alert").filter({
|
||||
hasText: "The audio could not be transcribed.",
|
||||
});
|
||||
|
|
@ -409,7 +409,7 @@ suite.define(() => {
|
|||
console.info(
|
||||
`[video-talk-e2e] preview=live,width:${dimensions.width},height:${dimensions.height}`,
|
||||
);
|
||||
await captureVideoTalkProof(page, "02-live-camera-preview.png");
|
||||
await captureVideoTalkProof(suite, page, "02-live-camera-preview.png");
|
||||
|
||||
await dispatchOpenAiTalkEvent(page, {
|
||||
type: "response.done",
|
||||
|
|
@ -476,7 +476,7 @@ suite.define(() => {
|
|||
);
|
||||
expect(trackStates).toHaveLength(2);
|
||||
expect(trackStates?.every((state) => state === "ended")).toBe(true);
|
||||
await captureVideoTalkProof(page, "04-after-video-talk-stop.png");
|
||||
await captureVideoTalkProof(suite, page, "04-after-video-talk-stop.png");
|
||||
console.info("[video-talk-e2e] stop=preview-removed,tracks:ended+ended");
|
||||
});
|
||||
});
|
||||
|
|
@ -604,7 +604,7 @@ suite.define(() => {
|
|||
"talk.catalog",
|
||||
"talk.client.create",
|
||||
]);
|
||||
await captureVideoTalkProof(page, "05-gemini-live-camera-preview.png");
|
||||
await captureVideoTalkProof(suite, page, "05-gemini-live-camera-preview.png");
|
||||
console.info(
|
||||
"[video-talk-e2e] gemini=realtimeInput.video+functionResponse,gateway_frame_requests:0",
|
||||
);
|
||||
|
|
@ -673,7 +673,7 @@ suite.define(() => {
|
|||
await expect
|
||||
.poll(() => page.getByRole("button", { name: "Turn camera on" }).isVisible())
|
||||
.toBe(true);
|
||||
await captureVideoTalkProof(page, "03-camera-permission-blocked.png");
|
||||
await captureVideoTalkProof(suite, page, "03-camera-permission-blocked.png");
|
||||
console.info("[video-talk-e2e] camera_denial=actionable,no-audio-fallback");
|
||||
});
|
||||
});
|
||||
|
|
@ -834,7 +834,7 @@ suite.define(() => {
|
|||
await expect
|
||||
.poll(() => gateway.getRequests("talk.session.close").then((requests) => requests.length))
|
||||
.toBe(1);
|
||||
await captureComposerProof(page, "relay-input-backpressure-error.png");
|
||||
await captureComposerProof(suite, page, "relay-input-backpressure-error.png");
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
|
||||
|
|
@ -419,32 +418,44 @@ export async function installOversizedWebRtcSdpFixture(page: Page) {
|
|||
});
|
||||
}
|
||||
|
||||
export async function captureComposerProof(page: Page, fileName: string) {
|
||||
const artifactDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "voice-controls");
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
export async function captureComposerProof(
|
||||
owner: { readonly artifactDir: string },
|
||||
page: Page,
|
||||
fileName: string,
|
||||
) {
|
||||
const artifactDir = path.join(owner.artifactDir, "voice-controls");
|
||||
await page
|
||||
.locator(".agent-chat__composer-shell")
|
||||
.screenshot({ path: path.join(artifactDir, fileName) });
|
||||
}
|
||||
|
||||
export async function captureMicrophoneLossProof(page: Page, fileName: string) {
|
||||
const artifactDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "voice-controls");
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
export async function captureMicrophoneLossProof(
|
||||
owner: { readonly artifactDir: string },
|
||||
page: Page,
|
||||
fileName: string,
|
||||
) {
|
||||
const artifactDir = path.join(owner.artifactDir, "voice-controls");
|
||||
// The error floats above the composer bounds; retain the real chat context.
|
||||
await page.screenshot({ path: path.join(artifactDir, fileName), fullPage: true });
|
||||
}
|
||||
|
||||
export async function captureVideoTalkProof(page: Page, fileName: string) {
|
||||
const artifactDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "video-talk");
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
export async function captureVideoTalkProof(
|
||||
owner: { readonly artifactDir: string },
|
||||
page: Page,
|
||||
fileName: string,
|
||||
) {
|
||||
const artifactDir = path.join(owner.artifactDir, "video-talk");
|
||||
await page
|
||||
.locator(".agent-chat__composer-shell")
|
||||
.screenshot({ path: path.join(artifactDir, fileName) });
|
||||
}
|
||||
|
||||
export async function captureWebRtcSdpAlertProof(page: Page, fileName: string) {
|
||||
const artifactDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "webrtc-sdp");
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
export async function captureWebRtcSdpAlertProof(
|
||||
owner: { readonly artifactDir: string },
|
||||
page: Page,
|
||||
fileName: string,
|
||||
) {
|
||||
const artifactDir = path.join(owner.artifactDir, "webrtc-sdp");
|
||||
await page
|
||||
.locator('.agent-chat__talk-status[role="alert"]')
|
||||
.screenshot({ path: path.join(artifactDir, fileName) });
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ suite.define(() => {
|
|||
|
||||
const alert = page.locator('.agent-chat__talk-status[role="alert"]');
|
||||
await expect.poll(() => alert.textContent()).toContain("Realtime WebRTC setup failed (502)");
|
||||
await captureWebRtcSdpAlertProof(page, "01-http-failure-alert.png");
|
||||
await captureWebRtcSdpAlertProof(suite, page, "01-http-failure-alert.png");
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
|
|
@ -109,7 +109,7 @@ suite.define(() => {
|
|||
await expect
|
||||
.poll(() => alert.textContent())
|
||||
.toContain("Realtime WebRTC SDP answer: text response exceeds 262144 bytes");
|
||||
await captureWebRtcSdpAlertProof(page, "02-oversized-answer-alert.png");
|
||||
await captureWebRtcSdpAlertProof(suite, page, "02-oversized-answer-alert.png");
|
||||
await expect
|
||||
.poll(() =>
|
||||
page.evaluate(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import path from "node:path";
|
||||
// Control UI E2E coverage for legacy WebKit media errors.
|
||||
import { expect, it } from "vitest";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
|
|
@ -37,9 +38,13 @@ suite.define(() => {
|
|||
.toBe("The selected microphone is unavailable. Choose another input or System default.");
|
||||
expect(await gateway.getRequests("talk.client.create")).toHaveLength(0);
|
||||
expect(await gateway.getRequests("talk.session.close")).toHaveLength(0);
|
||||
await captureComposerProof(page, "webkit-selected-microphone-unavailable-composer.png");
|
||||
await captureComposerProof(
|
||||
suite,
|
||||
page,
|
||||
"webkit-selected-microphone-unavailable-composer.png",
|
||||
);
|
||||
await page.getByRole("alert").screenshot({
|
||||
path: ".artifacts/control-ui-e2e/voice-controls/webkit-selected-microphone-alert.png",
|
||||
path: path.join(suite.artifactDir, "voice-controls/webkit-selected-microphone-alert.png"),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Control UI tests keep build identity readable at UTF-16 truncation boundaries.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway, startControlUiE2eServer } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -25,12 +25,12 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const uiProofArtifactDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"build-info-unicode",
|
||||
);
|
||||
let uiProofArtifactDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProofEnabled) {
|
||||
uiProofArtifactDir = createControlUiE2eArtifactDir("build-info-unicode");
|
||||
}
|
||||
});
|
||||
|
||||
const RAW_BRANCH = `${"a".repeat(12)}😀${"b".repeat(85)}😀suffix`;
|
||||
const NORMALIZED_BRANCH = `${"a".repeat(12)}😀${"b".repeat(85)}`;
|
||||
|
|
@ -128,7 +128,6 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(uiProofArtifactDir, { recursive: true });
|
||||
await identityCard.screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(uiProofArtifactDir, "00-footer-custom-build-identity.png"),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -12,12 +13,12 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const proofVariant = process.env.OPENCLAW_PICKER_PROOF_VARIANT;
|
||||
const proofDirectory = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"channel-wizard-option-contrast",
|
||||
);
|
||||
let proofDirectory: string;
|
||||
beforeEach(() => {
|
||||
if (proofVariant) {
|
||||
proofDirectory = createControlUiE2eArtifactDir("channel-wizard-option-contrast");
|
||||
}
|
||||
});
|
||||
|
||||
suite.define(() => {
|
||||
it("keeps option subtext legible and keyboard focus visible in forced colors", async () => {
|
||||
|
|
@ -101,7 +102,6 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
if (proofVariant) {
|
||||
await mkdir(proofDirectory, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Control UI tests cover WhatsApp logout feedback against a mocked Gateway.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway, waitForConfirmModal } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -14,24 +14,21 @@ const suite = createControlUiE2eSuite({
|
|||
const QR_DATA_URL =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9WlY9Z8AAAAASUVORK5CYII=";
|
||||
const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const uiProofArtifactDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"channels-save-failure",
|
||||
);
|
||||
const wizardUiProofArtifactDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"channel-wizard-continue-spinner",
|
||||
);
|
||||
let uiProofArtifactDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProofEnabled) {
|
||||
uiProofArtifactDir = createControlUiE2eArtifactDir("channels-save-failure");
|
||||
}
|
||||
});
|
||||
let wizardUiProofArtifactDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProofEnabled) {
|
||||
wizardUiProofArtifactDir = createControlUiE2eArtifactDir("channel-wizard-continue-spinner");
|
||||
}
|
||||
});
|
||||
|
||||
suite.define(() => {
|
||||
it("shows rejected channel configuration saves in the open editor without losing the draft", async () => {
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(uiProofArtifactDir, { recursive: true });
|
||||
}
|
||||
await suite.withPage(
|
||||
{
|
||||
locale: "en-US",
|
||||
|
|
@ -185,7 +182,6 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(uiProofArtifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
@ -272,7 +268,6 @@ suite.define(() => {
|
|||
await firstConfirm.getByRole("button", { name: "Cancel" }).focus();
|
||||
await page.keyboard.press("Escape");
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(uiProofArtifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
@ -377,9 +372,6 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("preserves standard channel details and the complete Telegram setup wizard", async () => {
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(wizardUiProofArtifactDir, { recursive: true });
|
||||
}
|
||||
await suite.withPage({ locale: "en-US", serviceWorkers: "block" }, async ({ page }) => {
|
||||
const channelEntries = [
|
||||
["discord", "Discord"],
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
chatSessionListResponse,
|
||||
createChatFlowE2eSuite,
|
||||
|
|
@ -14,10 +14,10 @@ const suite = createChatFlowE2eSuite();
|
|||
|
||||
suite.define(() => {
|
||||
it("restores active commentary when an evicted session revalidates from its cursor", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-active-cursor-replay", artifactRoot)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
...(artifactDir
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, type Page } from "playwright/test";
|
||||
import { it } from "vitest";
|
||||
import { beforeEach, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
installMockGateway,
|
||||
type MockGatewayControls,
|
||||
|
|
@ -16,7 +16,12 @@ const suite = createControlUiE2eSuite({
|
|||
`Playwright Chromium is required for active-turn recovery proof at ${executablePath}`,
|
||||
});
|
||||
|
||||
const proofDir = path.resolve(".artifacts/control-ui-e2e/active-turn-recovery");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("active-turn-recovery");
|
||||
}
|
||||
});
|
||||
const captureProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
type ActiveRunSnapshotOptions = {
|
||||
events?: unknown[];
|
||||
|
|
@ -30,7 +35,6 @@ async function capture(page: Page, name: string): Promise<void> {
|
|||
if (!captureProof) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(proofDir, `${name}.png`), fullPage: true });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -84,9 +84,11 @@ suite.define(() => {
|
|||
|
||||
expect(rowKeys[0]).toMatch(/^agent-run:/u);
|
||||
expect(rowKeys).toEqual([rowKeys[0], rowKeys[0], rowKeys[0]]);
|
||||
const artifactDir = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-agent-run-transcript", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
await page.locator(".chat-thread-inner").screenshot({
|
||||
path: path.join(artifactDir, "restart-recovery-run-frame.png"),
|
||||
});
|
||||
|
|
@ -249,9 +251,11 @@ suite.define(() => {
|
|||
await page.goto(`${suite.server.baseUrl}chat`);
|
||||
const transcript = page.locator(".chat-thread-inner");
|
||||
await transcript.getByText("Caption ready for the second run.", { exact: true }).waitFor();
|
||||
const artifactDir = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-agent-run-transcript", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
const firstNarration = transcript.getByText(
|
||||
"I’ll create the launch card and check the existing style first.",
|
||||
{ exact: true },
|
||||
|
|
@ -445,9 +449,11 @@ suite.define(() => {
|
|||
});
|
||||
expect(overlappingRows).toEqual([]);
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-agent-run-transcript", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
path: path.join(artifactDir, "cross-session-run-isolation.png"),
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
const suite = createControlUiE2eSuite({ name: "Attachment failure announcements" });
|
||||
const captureProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(process.cwd(), ".artifacts/control-ui-e2e/attachment-announcement");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("attachment-announcement");
|
||||
}
|
||||
});
|
||||
|
||||
suite.define(() => {
|
||||
it.each(["ordinary", "completed"])(
|
||||
|
|
@ -39,7 +45,6 @@ suite.define(() => {
|
|||
expect(await announcement.getAttribute("aria-atomic")).toBe("true");
|
||||
expect(await announcement.textContent()).toBe("");
|
||||
if (captureProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(proofDir, `${flow}-initial.png`) });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Control UI E2E tests cover attributed chat identity placement.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, type Locator, type Page } from "playwright/test";
|
||||
import { it } from "vitest";
|
||||
import { beforeEach, it } from "vitest";
|
||||
// Control UI E2E tests cover attributed chat identity placement.
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { controlUiSessionUrl, installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -11,16 +11,19 @@ const suite = createControlUiE2eSuite({
|
|||
startServerBeforeBrowser: true,
|
||||
});
|
||||
|
||||
function resolveArtifactDir(): string | undefined {
|
||||
return process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim() || undefined;
|
||||
}
|
||||
let proofArtifactDir: string | undefined;
|
||||
beforeEach(() => {
|
||||
const parent = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
proofArtifactDir = parent
|
||||
? createControlUiE2eArtifactDir("chat-attributed-identity", parent)
|
||||
: undefined;
|
||||
});
|
||||
|
||||
async function captureProof(page: Page, name: string) {
|
||||
const artifactDir = resolveArtifactDir();
|
||||
const artifactDir = proofArtifactDir;
|
||||
if (!artifactDir) {
|
||||
return;
|
||||
}
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(artifactDir, name),
|
||||
|
|
@ -66,10 +69,7 @@ function expectStableNamePosition(
|
|||
|
||||
suite.define(() => {
|
||||
it("uses one avatar placement and keeps shared-thread authors readable", async () => {
|
||||
const artifactDir = resolveArtifactDir();
|
||||
if (artifactDir) {
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
const artifactDir = proofArtifactDir;
|
||||
const context = await suite.browser.newContext({
|
||||
viewport: { height: 760, width: 1180 },
|
||||
...(artifactDir
|
||||
|
|
@ -446,10 +446,10 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("keeps an attributed failed send in the transcript with one-line retry metadata", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_BUBBLE_DELIVERY_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
const artifactRoot = process.env.OPENCLAW_BUBBLE_DELIVERY_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("bubble-delivery", artifactRoot)
|
||||
: undefined;
|
||||
const context = await suite.browser.newContext({ viewport: { height: 760, width: 1180 } });
|
||||
const page = await context.newPage();
|
||||
const sender = {
|
||||
|
|
@ -554,10 +554,7 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("keeps missing local-viewer avatar initials through a live rerender", async () => {
|
||||
const artifactDir = resolveArtifactDir();
|
||||
if (artifactDir) {
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
const artifactDir = proofArtifactDir;
|
||||
const context = await suite.browser.newContext({
|
||||
viewport: { height: 760, width: 1180 },
|
||||
...(artifactDir
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type Page } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { beforeEach, afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
|
|
@ -17,7 +17,12 @@ const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM
|
|||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const captureProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofStage = process.env.OPENCLAW_CODE_FENCE_PROOF_STAGE ?? "after";
|
||||
const artifactDir = path.resolve(process.cwd(), ".artifacts/control-ui-e2e/chat-code-block-fences");
|
||||
let artifactDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureProof) {
|
||||
artifactDir = createControlUiE2eArtifactDir("chat-code-block-fences");
|
||||
}
|
||||
});
|
||||
|
||||
function fencedJson(lineCount: number): string {
|
||||
const values = Array.from({ length: lineCount - 2 }, (_, index) => ` ${index},`);
|
||||
|
|
@ -62,9 +67,6 @@ describeControlUiE2e("Control UI fenced code blocks", () => {
|
|||
if (!chromiumAvailable) {
|
||||
throw new Error(`Playwright Chromium is unavailable at ${chromiumExecutablePath}`);
|
||||
}
|
||||
if (captureProof) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
server = await startControlUiE2eServer(undefined, { source: true });
|
||||
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -70,9 +70,11 @@ suite.define(() => {
|
|||
await outside.focus();
|
||||
await trigger.click();
|
||||
expect(await outside.evaluate((element) => document.activeElement === element)).toBe(true);
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-composer-accessory-focus", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir && triggerSelector.startsWith(".context-usage")) {
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
const composerBox = await composer.boundingBox();
|
||||
const popoverBox = await composer.locator(".context-usage__popover").boundingBox();
|
||||
if (!composerBox || !popoverBox) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -141,13 +142,15 @@ suite.define(() => {
|
|||
await dropdown.locator('[value="open-skills"]').click();
|
||||
await expect.poll(() => dropdown.getAttribute("data-view")).toBe("skills");
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-composer-capability-menu-height", artifactDirParent)
|
||||
: undefined;
|
||||
const captureStage = process.env.OPENCLAW_UI_E2E_CAPTURE_STAGE?.trim();
|
||||
const capture = async (view: string, theme: "dark" | "light") => {
|
||||
if (!artifactDir || !captureStage) {
|
||||
return;
|
||||
}
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
await page.evaluate((mode) => {
|
||||
document.documentElement.dataset.themeMode = mode;
|
||||
}, theme);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Control UI E2E tests cover chat composer catalog discovery.
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
controlUiSessionUrl,
|
||||
installMockGateway,
|
||||
|
|
@ -238,7 +239,10 @@ suite.define(() => {
|
|||
await expect.poll(() => composer.locator("textarea").isDisabled()).toBe(true);
|
||||
expect(await gateway.getRequests("chat.send")).toHaveLength(0);
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-composer-catalog", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await composer.screenshot({
|
||||
animations: "disabled",
|
||||
|
|
@ -467,7 +471,10 @@ suite.define(() => {
|
|||
await expect
|
||||
.poll(() => composer.locator("[data-chat-model-catalog-state]").textContent())
|
||||
.toContain("No models available");
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-composer-catalog", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
|
|
@ -546,7 +553,10 @@ suite.define(() => {
|
|||
await expect
|
||||
.poll(() => composer.locator('[data-chat-model-option="openai/gpt-5.6-luna"]').isVisible())
|
||||
.toBe(true);
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-composer-catalog", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
|
|
@ -606,7 +616,10 @@ suite.define(() => {
|
|||
.poll(() => textarea.evaluate((node) => node.matches(":placeholder-shown")))
|
||||
.toBe(true);
|
||||
await expect.poll(() => textarea.getAttribute("placeholder")).toContain("Message");
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-composer-catalog", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await page.locator(".agent-chat__composer-shell").screenshot({
|
||||
animations: "disabled",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Control UI E2E tests cover the redesigned chat composer.
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -25,7 +26,10 @@ suite.define(() => {
|
|||
] as const)(
|
||||
"keeps $reason model availability honest in the composer and picker",
|
||||
async ({ reason, blocked, message }) => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-composer-redesign", artifactRoot)
|
||||
: undefined;
|
||||
await suite.withPage(
|
||||
{
|
||||
viewport: { width: 1280, height: 900 },
|
||||
|
|
@ -189,7 +193,10 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("keeps the model in the bottom bar, session settings in the header, and holds send beside the microphone in every input state", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-composer-redesign", artifactRoot)
|
||||
: undefined;
|
||||
const pageOptions = {
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
...(artifactDir
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { mkdir, rm } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { decodeResumeHandoff } from "../../../src/shared/resume-handoff.js";
|
||||
import type { ChatPaneElement } from "../pages/chat/route-draft-focus-handoff.ts";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
controlUiSessionPath,
|
||||
controlUiSessionUrl,
|
||||
|
|
@ -18,7 +18,6 @@ const suite = createControlUiE2eSuite({
|
|||
`Playwright Chromium is not installed at ${executablePath}. Run \`pnpm --dir ui exec playwright install chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
|
||||
});
|
||||
|
||||
const artifactDir = path.resolve(process.cwd(), ".artifacts/control-ui-e2e/header-session-menu");
|
||||
const basePath = new URL("/nested/$&;=()+,![]{}'`/%25PATH%25", "http://localhost").pathname;
|
||||
const agentId = "runner";
|
||||
const sessionKey = `agent:${agentId}:main-'"$&;|<>^()%![]{}\\\`-%PATH%`;
|
||||
|
|
@ -67,8 +66,7 @@ const compactManagementActions = sharedManagementActions.filter(
|
|||
|
||||
suite.define(() => {
|
||||
it("shows, copies, and retires a credential-free exact continuation command", async () => {
|
||||
await rm(artifactDir, { recursive: true, force: true });
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
const artifactDir = createControlUiE2eArtifactDir("header-session-menu");
|
||||
await suite.withPage(
|
||||
{
|
||||
locale: "en-US",
|
||||
|
|
@ -164,7 +162,7 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("keeps the canonical session actions reachable in the mobile header menu", async () => {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
const artifactDir = createControlUiE2eArtifactDir("header-session-menu");
|
||||
await suite.withPage(
|
||||
{
|
||||
locale: "en-US",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,8 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { beforeEach, afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
controlUiE2eWaitTimeoutMs,
|
||||
|
|
@ -17,14 +18,16 @@ const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.
|
|||
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
||||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const artifactDir = path.resolve(process.cwd(), ".artifacts/control-ui-e2e/chat-file-links");
|
||||
let artifactDir: string;
|
||||
beforeEach(() => {
|
||||
artifactDir = createControlUiE2eArtifactDir("chat-file-links");
|
||||
});
|
||||
|
||||
let browser: Browser;
|
||||
let server: ControlUiE2eServer;
|
||||
|
||||
describeControlUiE2e("Control UI chat file links", () => {
|
||||
beforeAll(async () => {
|
||||
fs.mkdirSync(artifactDir, { recursive: true });
|
||||
server = await startControlUiE2eServer();
|
||||
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { waitForControlUiGatewayReconnecting } from "../test-helpers/control-ui-e2e-readiness.ts";
|
||||
import {
|
||||
chatSessionListResponse,
|
||||
|
|
@ -187,8 +187,7 @@ suite.define(() => {
|
|||
});
|
||||
const startupIndicator = page.locator('.chat-working-indicator[role="status"]');
|
||||
if (process.env.OPENCLAW_CAPTURE_UI_PROOF === "1") {
|
||||
const startupProofDir = path.resolve(".artifacts/control-ui-e2e/duplicate-session-naming");
|
||||
await mkdir(startupProofDir, { recursive: true });
|
||||
const startupProofDir = path.join(suite.artifactDir, "duplicate-session-naming");
|
||||
await page.screenshot({ path: path.join(startupProofDir, "steer.png"), fullPage: true });
|
||||
}
|
||||
await expect.poll(() => startupIndicator.textContent()).not.toContain("Naming worktree…");
|
||||
|
|
@ -483,9 +482,11 @@ suite.define(() => {
|
|||
)
|
||||
.toEqual([initialText, beforeText, steerText, afterText]);
|
||||
} finally {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.active-run-follow-ups", artifactDirParent)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(artifactDir, `steer-history-${historyOrder}-live-event.png`),
|
||||
|
|
@ -544,9 +545,11 @@ suite.define(() => {
|
|||
state: "delta",
|
||||
});
|
||||
const capture = async (name: string) => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.active-run-follow-ups", artifactDirParent)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(artifactDir, `steer-split-commentary-${name}.png`),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Locator, Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { createChatFlowE2eSuite, installMockGateway } from "./chat-flow.test-support.ts";
|
||||
|
||||
const suite = createChatFlowE2eSuite();
|
||||
|
|
@ -99,10 +99,10 @@ suite.define(() => {
|
|||
it.each(["tool-diff", "selection", "agent-id"] as const)(
|
||||
"reports clipboard failure from the %s action",
|
||||
async (surface) => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.clipboard", artifactDirParent)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
colorScheme: "light",
|
||||
locale: "en-US",
|
||||
|
|
@ -399,9 +399,11 @@ suite.define(() => {
|
|||
});
|
||||
expect(await gateway.getRequests("chat.send")).toHaveLength(0);
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.clipboard", artifactDirParent)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(artifactDir, `clipboard-${action}-failure.png`),
|
||||
|
|
@ -454,9 +456,11 @@ suite.define(() => {
|
|||
});
|
||||
expect(await gateway.getRequests("chat.send")).toHaveLength(0);
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.clipboard", artifactDirParent)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(artifactDir, "clipboard-assistant-code-failure.png"),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
createChatFlowE2eSuite,
|
||||
controlUiSessionUrl,
|
||||
|
|
@ -8,16 +8,16 @@ import {
|
|||
} from "./chat-flow.test-support.ts";
|
||||
|
||||
const suite = createChatFlowE2eSuite();
|
||||
const dynamicCatalogProofDir =
|
||||
process.env.OPENCLAW_CAPTURE_UI_PROOF === "1"
|
||||
? path.join(process.cwd(), ".artifacts", "control-ui-e2e", "dynamic-catalog-convergence")
|
||||
: null;
|
||||
let dynamicCatalogProofDir: string | null;
|
||||
beforeEach(() => {
|
||||
dynamicCatalogProofDir =
|
||||
process.env.OPENCLAW_CAPTURE_UI_PROOF === "1"
|
||||
? createControlUiE2eArtifactDir("dynamic-catalog-convergence")
|
||||
: null;
|
||||
});
|
||||
|
||||
suite.define(() => {
|
||||
it("converges Chat reasoning and context metadata after dynamic catalog discovery", async () => {
|
||||
if (dynamicCatalogProofDir) {
|
||||
await mkdir(dynamicCatalogProofDir, { recursive: true });
|
||||
}
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
|
|
@ -7,11 +6,12 @@ import { createChatFlowE2eSuite, installMockGateway } from "./chat-flow.test-sup
|
|||
const suite = createChatFlowE2eSuite();
|
||||
|
||||
async function captureHistoryIssuanceProof(page: Page, name: string): Promise<void> {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim()
|
||||
? suite.artifactDir
|
||||
: undefined;
|
||||
if (!artifactDir) {
|
||||
return;
|
||||
}
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({ fullPage: true, path: path.join(artifactDir, `${name}.png`) });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import type { ApplicationContext } from "../app/context.ts";
|
||||
import type { ChatQueueItem } from "../lib/chat/chat-types.ts";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
chatSessionListResponse,
|
||||
controlUiSessionUrl,
|
||||
|
|
@ -134,10 +134,10 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("restores reasoning and tool activity after navigating away from a session", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.history-recovery", artifactDirParent)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
...(artifactDir
|
||||
|
|
@ -425,7 +425,10 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("keeps evicted paginated history stable when returning to a session", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.history-recovery", artifactDirParent)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
...(artifactDir
|
||||
|
|
@ -695,7 +698,10 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("stores new input while offline and sends it after reconnect", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.history-recovery", artifactDirParent)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
...(artifactDir
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createPlaybackMediaFixture } from "../../../test/fixtures/media-playback.js";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { createChatFlowE2eSuite, installMockGateway } from "./chat-flow.test-support.ts";
|
||||
|
||||
const suite = createChatFlowE2eSuite();
|
||||
|
||||
suite.define(() => {
|
||||
it("allows tilde local media previews when the preview root home contains a literal $ pattern", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.local-media-dollar-home", artifactDirParent)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
@ -92,7 +95,6 @@ suite.define(() => {
|
|||
.toBeGreaterThanOrEqual(1);
|
||||
expect(await page.getByText("Outside allowed folders").count()).toBe(0);
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(artifactDir, "local-media-dollar-home-allowed.png"),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
import { createPlaybackMediaFixture } from "../../../test/fixtures/media-playback.js";
|
||||
import { useAutoCleanupTempDirTracker } from "../../../test/helpers/temp-dir.ts";
|
||||
import {
|
||||
buildLocalWebchatAudioMessage,
|
||||
captureUiProofEnabled,
|
||||
|
|
@ -10,12 +11,12 @@ import {
|
|||
expectDefined,
|
||||
installMockGateway,
|
||||
installPlainHttpClipboardCapture,
|
||||
managedImageCacheProofDir,
|
||||
waitForChatScrollIdle,
|
||||
} from "./chat-flow.test-support.ts";
|
||||
import { openChatSidePanelType } from "./chat-side-panel.test-support.ts";
|
||||
|
||||
const suite = createChatFlowE2eSuite();
|
||||
const mediaTempDirs = useAutoCleanupTempDirTracker(afterEach);
|
||||
|
||||
suite.define(() => {
|
||||
it("exposes an assistant document download with its Unicode filename and ticketed URL", async () => {
|
||||
|
|
@ -89,13 +90,17 @@ suite.define(() => {
|
|||
},
|
||||
{
|
||||
kind: "audio",
|
||||
source: `FILE:${path.join(managedImageCacheProofDir, "bootstrap-structured-audio.mp3")}`,
|
||||
source: "bootstrap-structured-audio.mp3",
|
||||
ticket: "ticket-bootstrap-structured-audio",
|
||||
structured: true,
|
||||
},
|
||||
] as const)(
|
||||
"renders local assistant $kind through server metadata before preview roots load",
|
||||
async ({ kind, source, ticket, ...options }) => {
|
||||
async ({ kind, source: fixtureSource, ticket, ...options }) => {
|
||||
const source =
|
||||
"structured" in options
|
||||
? `FILE:${path.join(mediaTempDirs.make("control-ui-audio-"), fixtureSource)}`
|
||||
: fixtureSource;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
@ -200,12 +205,10 @@ suite.define(() => {
|
|||
.toBe(180);
|
||||
}
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
if (process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim()) {
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(artifactDir, `bootstrap-local-${kind}-${ticket}.png`),
|
||||
path: path.join(suite.artifactDir, `bootstrap-local-${kind}-${ticket}.png`),
|
||||
});
|
||||
}
|
||||
if (process.env.OPENCLAW_BEHAVIOR_PROOF === "1") {
|
||||
|
|
@ -286,12 +289,10 @@ suite.define(() => {
|
|||
expect(await page.locator(".chat-assistant-attachment-card audio").count()).toBe(0);
|
||||
expect(await page.locator(".chat-assistant-attachment-card__download").count()).toBe(0);
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
if (process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim()) {
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(artifactDir, `bootstrap-blocked-${code}.png`),
|
||||
path: path.join(suite.artifactDir, `bootstrap-blocked-${code}.png`),
|
||||
});
|
||||
}
|
||||
if (process.env.OPENCLAW_BEHAVIOR_PROOF === "1") {
|
||||
|
|
@ -432,7 +433,9 @@ suite.define(() => {
|
|||
viewport: { height: 900, width: 1280 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const proofDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const proofDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim()
|
||||
? suite.artifactDir
|
||||
: undefined;
|
||||
const managedAttachmentSource = (artifactId: string) =>
|
||||
`/api/chat/media/outgoing/agent%3Amain%3Amain/${artifactId.slice("artifact_managed_media_".length)}/full`;
|
||||
const attachments = [
|
||||
|
|
@ -548,7 +551,6 @@ suite.define(() => {
|
|||
expect(await page.getByText("Checking...", { exact: true }).count()).toBe(0);
|
||||
expect(((await page.locator("body").textContent()) ?? "").includes("MEDIA:")).toBe(false);
|
||||
if (proofDir) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(proofDir, "media-batch-skeletons.png") });
|
||||
}
|
||||
|
||||
|
|
@ -602,7 +604,9 @@ suite.define(() => {
|
|||
] as const)(
|
||||
"renders a $name image through the ticketed media route",
|
||||
async ({ source, workspaceDir, screenshotName }) => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim()
|
||||
? suite.artifactDir
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
@ -670,7 +674,6 @@ suite.define(() => {
|
|||
)
|
||||
.toBe(1);
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: `${artifactDir}/${screenshotName}.png`,
|
||||
|
|
@ -889,7 +892,7 @@ suite.define(() => {
|
|||
revokedBlobUrls: finalProof.revoked.length,
|
||||
};
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(managedImageCacheProofDir, { recursive: true });
|
||||
const managedImageCacheProofDir = path.join(suite.artifactDir, "managed-image-cache");
|
||||
await page.evaluate((summary) => {
|
||||
const panel = document.createElement("pre");
|
||||
panel.setAttribute("data-managed-image-cache-proof", "true");
|
||||
|
|
|
|||
|
|
@ -1,10 +1,8 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
captureUiProofEnabled,
|
||||
channelStopProofDir,
|
||||
chatSessionListResponse,
|
||||
controlUiSessionUrl,
|
||||
createChatFlowE2eSuite,
|
||||
|
|
@ -872,7 +870,7 @@ suite.define(() => {
|
|||
await expect.poll(() => page.getByRole("listbox").count()).toBe(0);
|
||||
expect(await composer.inputValue()).toBe("");
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(channelStopProofDir, { recursive: true });
|
||||
const channelStopProofDir = path.join(suite.artifactDir, "channel-stop");
|
||||
await page.screenshot({
|
||||
path: path.join(channelStopProofDir, "stopped.png"),
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -11,14 +11,16 @@ import {
|
|||
const suite = createChatFlowE2eSuite();
|
||||
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "model-picker-refresh");
|
||||
|
||||
async function screenshot(page: Page, name: string) {
|
||||
if (!captureUiProof) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({ animations: "disabled", path: path.join(proofDir, name) });
|
||||
await mkdir(path.join(suite.artifactDir, "model-picker-refresh"), { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(path.join(suite.artifactDir, "model-picker-refresh"), name),
|
||||
});
|
||||
}
|
||||
|
||||
suite.define(() => {
|
||||
|
|
@ -28,7 +30,12 @@ suite.define(() => {
|
|||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
...(captureUiProof
|
||||
? { recordVideo: { dir: proofDir, size: { height: 900, width: 1280 } } }
|
||||
? {
|
||||
recordVideo: {
|
||||
dir: path.join(suite.artifactDir, "model-picker-refresh"),
|
||||
size: { height: 900, width: 1280 },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
chatSessionListResponse,
|
||||
createChatFlowE2eSuite,
|
||||
|
|
@ -119,7 +120,10 @@ suite.define(() => {
|
|||
});
|
||||
await expect.poll(() => page.getByText(/not supported for/u).count()).toBe(0);
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.model-switch-reasoning", artifactDirParent)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await page.screenshot({
|
||||
path: `${artifactDir}/model-thinking-sync.png`,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { expect, it } from "vitest";
|
||||
import { t } from "../i18n/lib/translate.ts";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
chatSessionListResponse,
|
||||
createChatFlowE2eSuite,
|
||||
|
|
@ -484,7 +485,10 @@ suite.define(() => {
|
|||
},
|
||||
]);
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.models-reasoning", artifactDirParent)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await menu.screenshot({
|
||||
animations: "disabled",
|
||||
|
|
@ -682,7 +686,10 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("shows one canonical default model with matching inherited reasoning", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.models-reasoning", artifactDirParent)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
|
|||
|
|
@ -946,7 +946,7 @@ suite.define(() => {
|
|||
expect(await link.getAttribute("aria-current")).toBe("page");
|
||||
expect(await link.getAttribute("aria-describedby")).toBeNull();
|
||||
expect(await link.ariaSnapshot()).toContain(`link "${readableTitle}"`);
|
||||
await captureSessionAccessibilityProof(page, "after-derived-title");
|
||||
await captureSessionAccessibilityProof(suite, page, "after-derived-title");
|
||||
|
||||
const listCountBeforePatch = (await gateway.getRequests("sessions.list")).length;
|
||||
await row.hover();
|
||||
|
|
@ -966,7 +966,7 @@ suite.define(() => {
|
|||
await expect.poll(() => label.textContent()).toBe(readableTitle);
|
||||
expect(await link.getAttribute("aria-current")).toBe("page");
|
||||
expect(await link.ariaSnapshot()).toContain(`link "${readableTitle}"`);
|
||||
await captureSessionAccessibilityProof(page, "after-patch-refresh");
|
||||
await captureSessionAccessibilityProof(suite, page, "after-patch-refresh");
|
||||
} finally {
|
||||
await suite.closeBrowserContext(context);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
captureUiProofEnabled,
|
||||
|
|
@ -104,7 +105,7 @@ suite.define(() => {
|
|||
it("dispatches after its settings refresh while a later roster refresh is still pending", async () => {
|
||||
const context = await suite.newBrowserContext({
|
||||
...(captureUiProofEnabled
|
||||
? { recordVideo: { dir: ".artifacts/send-settings-wait/video" } }
|
||||
? { recordVideo: { dir: path.join(suite.artifactDir, "send-settings-wait", "video") } }
|
||||
: {}),
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
@ -158,7 +159,9 @@ suite.define(() => {
|
|||
.getByText("Settings applied and message delivered.")
|
||||
.waitFor();
|
||||
if (captureUiProofEnabled) {
|
||||
await page.screenshot({ path: ".artifacts/send-settings-wait/browser-after.png" });
|
||||
await page.screenshot({
|
||||
path: path.join(suite.artifactDir, "send-settings-wait", "browser-after.png"),
|
||||
});
|
||||
}
|
||||
await gateway.resolveDeferred("sessions.list");
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
createChatFlowE2eSuite,
|
||||
expectRequestCountStable,
|
||||
|
|
@ -160,7 +161,10 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("keeps edit, remove, and reorder outcomes exact through reconnect", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.queue-edit", artifactDirParent)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
...(artifactDir
|
||||
|
|
|
|||
|
|
@ -10,12 +10,6 @@ import {
|
|||
const suite = createChatFlowE2eSuite();
|
||||
|
||||
const QUEUED = ["review the migration", "then update the docs", "finally run the smoke"] as const;
|
||||
const failureReloadProofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"queue-reorder-failure-reload",
|
||||
);
|
||||
|
||||
suite.define(() => {
|
||||
it("reorders offline queued messages from the keyboard-focused handle", async () => {
|
||||
|
|
@ -65,7 +59,9 @@ suite.define(() => {
|
|||
|
||||
it("keeps the queue order consistent through a reload after a mid-reorder storage failure", async () => {
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(failureReloadProofDir, { recursive: true });
|
||||
await mkdir(path.join(suite.artifactDir, "queue-reorder-failure-reload"), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
|
|
@ -102,7 +98,9 @@ suite.define(() => {
|
|||
const queueText = () => page.locator(".chat-queue__item .chat-queue__text").allTextContents();
|
||||
expect(await queueText()).toEqual([...QUEUED]);
|
||||
if (captureUiProofEnabled) {
|
||||
await page.screenshot({ path: `${failureReloadProofDir}/01-queued-before-failure.png` });
|
||||
await page.screenshot({
|
||||
path: `${path.join(suite.artifactDir, "queue-reorder-failure-reload")}/01-queued-before-failure.png`,
|
||||
});
|
||||
}
|
||||
|
||||
// Break durable storage for the rest of this page's lifetime, then attempt
|
||||
|
|
@ -126,7 +124,7 @@ suite.define(() => {
|
|||
expect(await queueText()).toEqual([...QUEUED]);
|
||||
if (captureUiProofEnabled) {
|
||||
await page.screenshot({
|
||||
path: `${failureReloadProofDir}/02-order-unchanged-after-failure.png`,
|
||||
path: `${path.join(suite.artifactDir, "queue-reorder-failure-reload")}/02-order-unchanged-after-failure.png`,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -173,7 +171,7 @@ suite.define(() => {
|
|||
expect(await queueText()).toEqual([...QUEUED]);
|
||||
if (captureUiProofEnabled) {
|
||||
await page.screenshot({
|
||||
path: `${failureReloadProofDir}/03-consistent-order-after-reload.png`,
|
||||
path: `${path.join(suite.artifactDir, "queue-reorder-failure-reload")}/03-consistent-order-after-reload.png`,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -14,36 +14,23 @@ import {
|
|||
} from "./chat-flow.test-support.ts";
|
||||
|
||||
const suite = createChatFlowE2eSuite();
|
||||
const terminalMetadataProofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"remote-session-sidebar-metadata",
|
||||
);
|
||||
const sessionSecondRowProofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"session-status-second-row-implementation",
|
||||
);
|
||||
const subtitleStabilityProofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"sidebar-subtitle-stability",
|
||||
);
|
||||
|
||||
suite.define(() => {
|
||||
it("keeps a running subtitle and row height stable when its session is opened", async () => {
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(subtitleStabilityProofDir, { recursive: true });
|
||||
await mkdir(path.join(suite.artifactDir, "sidebar-subtitle-stability"), { recursive: true });
|
||||
}
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
...(captureUiProofEnabled
|
||||
? { recordVideo: { dir: subtitleStabilityProofDir, size: { height: 900, width: 1280 } } }
|
||||
? {
|
||||
recordVideo: {
|
||||
dir: path.join(suite.artifactDir, "sidebar-subtitle-stability"),
|
||||
size: { height: 900, width: 1280 },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
|
@ -100,7 +87,10 @@ suite.define(() => {
|
|||
if (captureUiProofEnabled) {
|
||||
await page.waitForTimeout(800);
|
||||
await secondRow.screenshot({
|
||||
path: path.join(subtitleStabilityProofDir, "01-running-before-open.png"),
|
||||
path: path.join(
|
||||
path.join(suite.artifactDir, "sidebar-subtitle-stability"),
|
||||
"01-running-before-open.png",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -116,14 +106,20 @@ suite.define(() => {
|
|||
if (captureUiProofEnabled) {
|
||||
await page.waitForTimeout(800);
|
||||
await secondRow.screenshot({
|
||||
path: path.join(subtitleStabilityProofDir, "02-running-after-open.png"),
|
||||
path: path.join(
|
||||
path.join(suite.artifactDir, "sidebar-subtitle-stability"),
|
||||
"02-running-after-open.png",
|
||||
),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await suite.closeBrowserContext(context);
|
||||
if (proofVideo) {
|
||||
await proofVideo.saveAs(
|
||||
path.join(subtitleStabilityProofDir, "sidebar-subtitle-stability.webm"),
|
||||
path.join(
|
||||
path.join(suite.artifactDir, "sidebar-subtitle-stability"),
|
||||
"sidebar-subtitle-stability.webm",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -131,14 +127,21 @@ suite.define(() => {
|
|||
|
||||
it("replaces an intermediate running subtitle with the unread final digest", async () => {
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(terminalMetadataProofDir, { recursive: true });
|
||||
await mkdir(path.join(suite.artifactDir, "remote-session-sidebar-metadata"), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
...(captureUiProofEnabled
|
||||
? { recordVideo: { dir: terminalMetadataProofDir, size: { height: 900, width: 1280 } } }
|
||||
? {
|
||||
recordVideo: {
|
||||
dir: path.join(suite.artifactDir, "remote-session-sidebar-metadata"),
|
||||
size: { height: 900, width: 1280 },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
|
@ -198,7 +201,10 @@ suite.define(() => {
|
|||
if (captureUiProofEnabled) {
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(terminalMetadataProofDir, "01-running-subtitle.png"),
|
||||
path: path.join(
|
||||
path.join(suite.artifactDir, "remote-session-sidebar-metadata"),
|
||||
"01-running-subtitle.png",
|
||||
),
|
||||
});
|
||||
}
|
||||
await gateway.setMethodResponse("sessions.list", completed);
|
||||
|
|
@ -223,7 +229,10 @@ suite.define(() => {
|
|||
if (captureUiProofEnabled) {
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(terminalMetadataProofDir, "02-final-reply-subtitle.png"),
|
||||
path: path.join(
|
||||
path.join(suite.artifactDir, "remote-session-sidebar-metadata"),
|
||||
"02-final-reply-subtitle.png",
|
||||
),
|
||||
});
|
||||
}
|
||||
const listRequests = await gateway.getRequests("sessions.list");
|
||||
|
|
@ -328,14 +337,21 @@ suite.define(() => {
|
|||
|
||||
it("keeps session titles on the first line and collapses rows that have no second line", async () => {
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(sessionSecondRowProofDir, { recursive: true });
|
||||
await mkdir(path.join(suite.artifactDir, "session-status-second-row-implementation"), {
|
||||
recursive: true,
|
||||
});
|
||||
}
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 900, width: 1280 },
|
||||
...(captureUiProofEnabled
|
||||
? { recordVideo: { dir: sessionSecondRowProofDir, size: { height: 900, width: 1280 } } }
|
||||
? {
|
||||
recordVideo: {
|
||||
dir: path.join(suite.artifactDir, "session-status-second-row-implementation"),
|
||||
size: { height: 900, width: 1280 },
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
});
|
||||
const page = await context.newPage();
|
||||
|
|
@ -407,7 +423,10 @@ suite.define(() => {
|
|||
expect(await busyRow.getAttribute("class")).toContain("sidebar-recent-session--single-line");
|
||||
if (captureUiProofEnabled) {
|
||||
await page.locator(".shell-nav").screenshot({
|
||||
path: path.join(sessionSecondRowProofDir, "00-default-hidden-preview.png"),
|
||||
path: path.join(
|
||||
path.join(suite.artifactDir, "session-status-second-row-implementation"),
|
||||
"00-default-hidden-preview.png",
|
||||
),
|
||||
});
|
||||
}
|
||||
await page.locator(".sidebar-session-toolbar .sidebar-session-sort").click();
|
||||
|
|
@ -442,7 +461,10 @@ suite.define(() => {
|
|||
if (captureUiProofEnabled) {
|
||||
await page.locator(".shell-nav").screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(sessionSecondRowProofDir, `indicators-${colorScheme}.png`),
|
||||
path: path.join(
|
||||
path.join(suite.artifactDir, "session-status-second-row-implementation"),
|
||||
`indicators-${colorScheme}.png`,
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -462,11 +484,17 @@ suite.define(() => {
|
|||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
path: path.join(sessionSecondRowProofDir, `01-second-row-endcap-${sidebarWidth}.png`),
|
||||
path: path.join(
|
||||
path.join(suite.artifactDir, "session-status-second-row-implementation"),
|
||||
`01-second-row-endcap-${sidebarWidth}.png`,
|
||||
),
|
||||
});
|
||||
await shellNav.screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(sessionSecondRowProofDir, `01-sidebar-${sidebarWidth}.png`),
|
||||
path: path.join(
|
||||
path.join(suite.artifactDir, "session-status-second-row-implementation"),
|
||||
`01-sidebar-${sidebarWidth}.png`,
|
||||
),
|
||||
});
|
||||
}
|
||||
badgeSizes.push(
|
||||
|
|
@ -586,7 +614,10 @@ suite.define(() => {
|
|||
if (captureUiProofEnabled) {
|
||||
await shellNav.screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(sessionSecondRowProofDir, "03-unread-hover.png"),
|
||||
path: path.join(
|
||||
path.join(suite.artifactDir, "session-status-second-row-implementation"),
|
||||
"03-unread-hover.png",
|
||||
),
|
||||
});
|
||||
}
|
||||
await unreadDot.waitFor({ state: "hidden" });
|
||||
|
|
@ -624,7 +655,10 @@ suite.define(() => {
|
|||
if (captureUiProofEnabled) {
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(sessionSecondRowProofDir, "02-hover-actions.png"),
|
||||
path: path.join(
|
||||
path.join(suite.artifactDir, "session-status-second-row-implementation"),
|
||||
"02-hover-actions.png",
|
||||
),
|
||||
});
|
||||
}
|
||||
await plainRow.waitFor();
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import type { ChatHost } from "../pages/chat/chat-send-contract.ts";
|
||||
import { CHAT_TRANSCRIPT_END_THRESHOLD_PX } from "../pages/chat/scroll.ts";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
chatThreadDistanceFromBottom,
|
||||
createChatFlowE2eSuite,
|
||||
|
|
@ -22,7 +22,10 @@ suite.define(() => {
|
|||
{ label: "desktop hover", mobile: false, viewport: { height: 900, width: 1280 } },
|
||||
{ label: "mobile tap", mobile: true, viewport: { height: 844, width: 390 } },
|
||||
])("shows turn metadata only after completion on $label", async ({ mobile, viewport }) => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.streaming", artifactDirParent)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
hasTouch: mobile,
|
||||
isMobile: mobile,
|
||||
|
|
@ -55,7 +58,6 @@ suite.define(() => {
|
|||
: { opacity: "1", pointerEvents: "auto" },
|
||||
);
|
||||
if (artifactDir && !mobile) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(artifactDir, "before-user-follow-up-actions-visible.png"),
|
||||
|
|
@ -152,7 +154,10 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("keeps a bottom-anchored transcript pinned while the composer grows", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.streaming", artifactDirParent)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
@ -194,7 +199,6 @@ suite.define(() => {
|
|||
).toBeLessThanOrEqual(CHAT_TRANSCRIPT_END_THRESHOLD_PX);
|
||||
}
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
path: path.join(artifactDir, "composer-resize-pinned.png"),
|
||||
|
|
@ -354,7 +358,10 @@ suite.define(() => {
|
|||
])(
|
||||
"keeps streamed text visible when a chat error terminates the turn on $label",
|
||||
async ({ label, viewport }) => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDirParent = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactDirParent
|
||||
? createControlUiE2eArtifactDir("chat-flow.streaming", artifactDirParent)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
@ -434,7 +441,6 @@ suite.define(() => {
|
|||
await page.locator(".chat-thread-inner").getByText(partialText, { exact: true }).count(),
|
||||
).toBe(1);
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(artifactDir, `terminal-partial-${label}.png`) });
|
||||
}
|
||||
const alert = page.locator(".chat-error");
|
||||
|
|
|
|||
|
|
@ -23,25 +23,7 @@ export {
|
|||
pauseVirtualClock,
|
||||
};
|
||||
|
||||
export const managedImageCacheProofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"managed-image-cache",
|
||||
);
|
||||
export const channelStopProofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"channel-stop",
|
||||
);
|
||||
export const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const sessionAccessibilityProofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"session-accessibility",
|
||||
);
|
||||
|
||||
export function createChatFlowE2eSuite() {
|
||||
return createControlUiE2eSuite({
|
||||
|
|
@ -178,11 +160,15 @@ export async function scrollChatThreadToTop(page: Page): Promise<void> {
|
|||
});
|
||||
}
|
||||
|
||||
export async function captureSessionAccessibilityProof(page: Page, name: string): Promise<void> {
|
||||
export async function captureSessionAccessibilityProof(
|
||||
owner: { readonly artifactDir: string },
|
||||
page: Page,
|
||||
name: string,
|
||||
): Promise<void> {
|
||||
if (!captureUiProofEnabled) {
|
||||
return;
|
||||
}
|
||||
await mkdir(sessionAccessibilityProofDir, { recursive: true });
|
||||
const sessionAccessibilityProofDir = path.join(owner.artifactDir, "session-accessibility");
|
||||
const sidebar = page.locator("openclaw-app-sidebar");
|
||||
await page.screenshot({
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,21 @@
|
|||
// Real-browser proof of frozen goal timing on the chat composer.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
const suite = createControlUiE2eSuite({ name: "Control UI goal elapsed time" });
|
||||
const captureProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const artifactDir = path.resolve(".artifacts/control-ui-e2e/goal-elapsed");
|
||||
let artifactDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureProof) {
|
||||
artifactDir = createControlUiE2eArtifactDir("goal-elapsed");
|
||||
}
|
||||
});
|
||||
|
||||
suite.define(() => {
|
||||
it("shows a paused goal's frozen duration before and after opening details and reloading", async () => {
|
||||
if (captureProof) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
await suite.withPage(
|
||||
{
|
||||
colorScheme: "light",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { requireRecord, requireString } from "./chat-flow.test-support.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
|
@ -10,14 +10,16 @@ const suite = createControlUiE2eSuite({
|
|||
trackBrowserContexts: true,
|
||||
});
|
||||
const captureProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(process.cwd(), ".artifacts/control-ui-e2e/codex-guardian-review");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("codex-guardian-review");
|
||||
}
|
||||
});
|
||||
const viewport = { height: 900, width: 1280 };
|
||||
|
||||
suite.define(() => {
|
||||
it("shows and settles a strict Guardian review in the active chat", async () => {
|
||||
if (captureProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
await suite.withPage(
|
||||
{
|
||||
locale: "en-US",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { mkdir, readFile } from "node:fs/promises";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
|
|
@ -248,15 +248,10 @@ suite.define(() => {
|
|||
await icon.waitFor();
|
||||
await icon.locator("svg").waitFor();
|
||||
await icon.evaluate((element) => element.setAttribute("data-recovery-host", "mounted"));
|
||||
const proofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"workspace-icon-recovery",
|
||||
);
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(proofDir, "fallback.png") });
|
||||
await page.screenshot({
|
||||
path: path.join(suite.artifactDir, "workspace-icon-recovery", "fallback.png"),
|
||||
});
|
||||
}
|
||||
|
||||
await icon.locator(".workspace-icon").waitFor({ timeout: 10_000 });
|
||||
|
|
@ -264,7 +259,9 @@ suite.define(() => {
|
|||
expect(requests).toBe(2);
|
||||
expect(await icon.getAttribute("data-recovery-host")).toBe("mounted");
|
||||
if (captureUiProofEnabled) {
|
||||
await page.screenshot({ path: path.join(proofDir, "recovered.png") });
|
||||
await page.screenshot({
|
||||
path: path.join(suite.artifactDir, "workspace-icon-recovery", "recovered.png"),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
await suite.closeBrowserContext(context);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { controlUiSessionUrl, installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -9,15 +9,14 @@ const suite = createControlUiE2eSuite({
|
|||
startServerBeforeBrowser: true,
|
||||
});
|
||||
|
||||
const outputDir = path.resolve(
|
||||
process.cwd(),
|
||||
process.env.OPENCLAW_CHAT_HEADER_CAPTURE_OUTPUT_DIR ??
|
||||
".artifacts/control-ui-e2e/chat-header-owner-presence",
|
||||
);
|
||||
suite.define(() => {
|
||||
it.each(["chat", "dashboard"] as const)(
|
||||
"deduplicates participants watching the %s",
|
||||
async (face) => {
|
||||
const outputDir = createControlUiE2eArtifactDir(
|
||||
"chat-header-owner-presence",
|
||||
process.env.OPENCLAW_CHAT_HEADER_CAPTURE_OUTPUT_DIR,
|
||||
);
|
||||
await suite.withPage(
|
||||
{
|
||||
locale: "en-US",
|
||||
|
|
@ -138,7 +137,6 @@ suite.define(() => {
|
|||
if (!clip) {
|
||||
throw new Error("Chat header did not expose a screenshot bounding box");
|
||||
}
|
||||
await mkdir(outputDir, { recursive: true });
|
||||
await page.screenshot({ animations: "disabled", clip, path: screenshotPath });
|
||||
process.stdout.write(`Chat header screenshot: ${screenshotPath}\n`);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { beforeAll, expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
chatSessionListResponse,
|
||||
controlUiSessionPath,
|
||||
|
|
@ -12,12 +12,12 @@ import {
|
|||
|
||||
const suite = createChatFlowE2eSuite();
|
||||
const captureProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"header-outcomes-followup",
|
||||
);
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("header-outcomes-followup");
|
||||
}
|
||||
});
|
||||
|
||||
async function capture(page: Page, name: string): Promise<void> {
|
||||
if (captureProof) {
|
||||
|
|
@ -97,12 +97,6 @@ async function startPlacementReclaim(
|
|||
}
|
||||
|
||||
suite.define(() => {
|
||||
beforeAll(async () => {
|
||||
if (captureProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not resurrect a reveal failure after navigating away", async () => {
|
||||
const context = await suite.newBrowserContext(proofContextOptions());
|
||||
const page = await context.newPage();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,15 @@
|
|||
// Control UI E2E covers pooling reply-less wake activity (heartbeats) into one rollup row.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
|
||||
let artifactDir: string | undefined;
|
||||
beforeEach(() => {
|
||||
const parent = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
artifactDir = parent
|
||||
? createControlUiE2eArtifactDir("chat-heartbeat-activity-rollup", parent)
|
||||
: undefined;
|
||||
});
|
||||
import { controlUiSessionUrl, installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { waitForChatScrollIdle } from "./chat-flow.test-support.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
|
@ -12,11 +20,9 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
async function captureProof(page: import("playwright").Page, name: string) {
|
||||
const artifactDir = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (!artifactDir) {
|
||||
return;
|
||||
}
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({ path: path.join(artifactDir, `${name}.png`), fullPage: true });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import {
|
||||
CHAT_SNAPSHOT_DB_NAME,
|
||||
CHAT_SNAPSHOT_STORE_NAME,
|
||||
} from "../pages/chat/session-snapshot-database.ts";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
createChatFlowE2eSuite,
|
||||
installMockGateway,
|
||||
|
|
@ -27,10 +27,10 @@ function historyMessage(seq: number, text: string) {
|
|||
|
||||
suite.define(() => {
|
||||
it("discards stale transcript geometry before restored history bootstrap", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await fs.mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-history-stale-geometry", artifactRoot)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { mkdir, readFile } from "node:fs/promises";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type BrowserContext } from "playwright";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { beforeEach, afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { finishElementAnimations } from "../test-helpers/animations.ts";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
|
|
@ -17,7 +18,12 @@ const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
|||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(process.cwd(), ".artifacts", "control-ui-e2e", "image-lightbox");
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProofEnabled) {
|
||||
proofDir = createControlUiE2eArtifactDir("image-lightbox");
|
||||
}
|
||||
});
|
||||
|
||||
let server: ControlUiE2eServer;
|
||||
let browser: Browser;
|
||||
|
|
@ -57,9 +63,6 @@ describeControlUiE2e("Control UI image lightbox", () => {
|
|||
const banner = await readFile(path.join(process.cwd(), "docs/assets/openclaw-banner-dark.png"));
|
||||
const bannerBase64 = banner.toString("base64");
|
||||
const dataUrl = `data:image/png;base64,${bannerBase64}`;
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
const context = await newContext({
|
||||
locale: "en-US",
|
||||
recordVideo: captureUiProofEnabled
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// Control UI regression proof for #99213: paste a large screenshot-like PNG through the
|
||||
// real chat composer and verify chat.send receives it without overflowing base64 handling.
|
||||
import { copyFile, mkdir, rm, writeFile } from "node:fs/promises";
|
||||
import { copyFile, rm, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures";
|
||||
import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
controlUiE2eWaitTimeoutMs,
|
||||
|
|
@ -18,7 +19,6 @@ const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.
|
|||
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
||||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const artifactDir = path.resolve(process.cwd(), ".artifacts/control-ui-e2e/chat-large-paste-99213");
|
||||
const viewport = { height: 900, width: 1280 };
|
||||
|
||||
let server: ControlUiE2eServer;
|
||||
|
|
@ -113,11 +113,8 @@ function toBase64(bytes: Uint8Array): string {
|
|||
return Buffer.from(bytes).toString("base64");
|
||||
}
|
||||
|
||||
async function newRecordedPage(label: string): Promise<RecordedPage> {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
const rawVideoDir = path.join(artifactDir, `${label}-raw`);
|
||||
await rm(rawVideoDir, { force: true, recursive: true });
|
||||
await mkdir(rawVideoDir, { recursive: true });
|
||||
async function newRecordedPage(artifactDir: string, label: string): Promise<RecordedPage> {
|
||||
const rawVideoDir = createControlUiE2eArtifactDir(`${label}-raw`, artifactDir);
|
||||
const browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
let context: BrowserContext | undefined;
|
||||
let page: Page | undefined;
|
||||
|
|
@ -139,12 +136,15 @@ async function newRecordedPage(label: string): Promise<RecordedPage> {
|
|||
await page?.close().catch(() => {});
|
||||
await context?.close().catch(() => {});
|
||||
await browser.close().catch(() => {});
|
||||
await rm(rawVideoDir, { force: true, recursive: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function closeRecordedPage(recorded: RecordedPage, label: string): Promise<string[]> {
|
||||
async function closeRecordedPage(
|
||||
recorded: RecordedPage,
|
||||
artifactDir: string,
|
||||
label: string,
|
||||
): Promise<string[]> {
|
||||
const video = recorded.page.video();
|
||||
const videos: string[] = [];
|
||||
try {
|
||||
|
|
@ -154,10 +154,11 @@ async function closeRecordedPage(recorded: RecordedPage, label: string): Promise
|
|||
const videoPath = path.join(artifactDir, `${label}.webm`);
|
||||
await copyFile(rawVideoPath, videoPath);
|
||||
videos.push(videoPath);
|
||||
// Failed finalization leaves the raw recording available for inspection.
|
||||
await rm(recorded.rawVideoDir, { force: true, recursive: true });
|
||||
}
|
||||
} finally {
|
||||
await recorded.browser.close().catch(() => {});
|
||||
await rm(recorded.rawVideoDir, { force: true, recursive: true });
|
||||
}
|
||||
return videos;
|
||||
}
|
||||
|
|
@ -177,13 +178,12 @@ describeControlUiE2e("Control UI #99213 large screenshot paste proof", () => {
|
|||
});
|
||||
|
||||
it("sends a roughly 2 MB PNG without overlapping transcript rows", async () => {
|
||||
await rm(artifactDir, { force: true, recursive: true });
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
const artifactDir = createControlUiE2eArtifactDir("chat-large-paste-99213");
|
||||
const pngBytes = createLargePngBytes(1_901_669);
|
||||
const imageBase64 = toBase64(pngBytes);
|
||||
const dataUrl = `data:image/png;base64,${imageBase64}`;
|
||||
const prompt = "proof: large Control UI clipboard image";
|
||||
const recorded = await newRecordedPage("large-paste");
|
||||
const recorded = await newRecordedPage(artifactDir, "large-paste");
|
||||
const screenshots: string[] = [];
|
||||
let videos: string[];
|
||||
|
||||
|
|
@ -270,7 +270,7 @@ describeControlUiE2e("Control UI #99213 large screenshot paste proof", () => {
|
|||
await recorded.page.screenshot({ fullPage: true, path: sentScreenshot });
|
||||
screenshots.push(sentScreenshot);
|
||||
} finally {
|
||||
videos = await closeRecordedPage(recorded, "large-paste");
|
||||
videos = await closeRecordedPage(recorded, artifactDir, "large-paste");
|
||||
}
|
||||
|
||||
const summary = {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
createChatFlowE2eSuite,
|
||||
installMockGateway,
|
||||
|
|
@ -11,10 +11,10 @@ const suite = createChatFlowE2eSuite();
|
|||
|
||||
suite.define(() => {
|
||||
it("keeps multiple live replies after their delayed prompt before history catches up", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
const artifactRoot = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-live-final-order", artifactRoot)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
@ -233,9 +233,11 @@ suite.define(() => {
|
|||
)
|
||||
.toBe(true);
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_CONTROL_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-live-final-order", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
path: path.join(artifactDir, "live-final-transcript-order.png"),
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { beforeEach, afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
|
|
@ -15,10 +15,12 @@ const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
|||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const captureProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const artifactDir = path.resolve(
|
||||
process.cwd(),
|
||||
".artifacts/control-ui-e2e/chat-markdown-table-interactions",
|
||||
);
|
||||
let artifactDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureProof) {
|
||||
artifactDir = createControlUiE2eArtifactDir("chat-markdown-table-interactions");
|
||||
}
|
||||
});
|
||||
|
||||
const wideTable = `| Service | Owner | Region | Status | Version | Deploy | Incidents | Notes |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
|
|
@ -32,9 +34,6 @@ describeControlUiE2e("Control UI Markdown table interactions", () => {
|
|||
if (!chromiumAvailable) {
|
||||
throw new Error(`Playwright Chromium is unavailable at ${chromiumExecutablePath}`);
|
||||
}
|
||||
if (captureProof) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
server = await startControlUiE2eServer(undefined, { source: true });
|
||||
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
// Real-browser proof for inline and context-menu chat message actions.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type Locator, type Page } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { beforeEach, afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
|
|
@ -16,7 +16,12 @@ const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
|||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const artifactDir = path.resolve(process.cwd(), ".artifacts/control-ui-e2e/chat-message-actions");
|
||||
let artifactDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
artifactDir = createControlUiE2eArtifactDir("chat-message-actions");
|
||||
}
|
||||
});
|
||||
|
||||
// Neutral filler line repeated to push the fixture past the transport
|
||||
// preview limit without embedding stale implementation narrative; the
|
||||
|
|
@ -154,9 +159,6 @@ describeControlUiE2e("Control UI chat message actions", () => {
|
|||
if (!chromiumAvailable) {
|
||||
throw new Error(`Playwright Chromium is unavailable at ${chromiumExecutablePath}`);
|
||||
}
|
||||
if (captureUiProof) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
server = await startControlUiE2eServer();
|
||||
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -129,7 +130,10 @@ suite.define(() => {
|
|||
await expect
|
||||
.poll(() => model.evaluate((node) => node === document.activeElement))
|
||||
.toBe(true);
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-model-controls", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir && [320, 393, 560, 1280].includes(width)) {
|
||||
await page.screenshot({
|
||||
path: `${artifactDir}/${route}-model-effort-${width}.png`,
|
||||
|
|
@ -209,7 +213,10 @@ suite.define(() => {
|
|||
),
|
||||
)
|
||||
.toBe(true);
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-model-controls", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await page.screenshot({
|
||||
path: `${artifactDir}/chat-model-effort-split.png`,
|
||||
|
|
@ -309,7 +316,10 @@ suite.define(() => {
|
|||
await expect
|
||||
.poll(() => effort.evaluate((node) => node === document.activeElement))
|
||||
.toBe(true);
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-model-controls", artifactRoot)
|
||||
: undefined;
|
||||
if (artifactDir) {
|
||||
await page.screenshot({
|
||||
path: `${artifactDir}/chat-speed-only-320.png`,
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { createChatFlowE2eSuite, installMockGateway } from "./chat-flow.test-support.ts";
|
||||
|
||||
const suite = createChatFlowE2eSuite();
|
||||
const sessionKey = "agent:main:main";
|
||||
const proofDir =
|
||||
process.env.OPENCLAW_CAPTURE_UI_PROOF === "1"
|
||||
? path.join(process.cwd(), ".artifacts", "control-ui-e2e", "chat-only-model")
|
||||
: null;
|
||||
let proofDir: string | null;
|
||||
beforeEach(() => {
|
||||
proofDir =
|
||||
process.env.OPENCLAW_CAPTURE_UI_PROOF === "1"
|
||||
? createControlUiE2eArtifactDir("chat-only-model")
|
||||
: null;
|
||||
});
|
||||
|
||||
const models = [
|
||||
{
|
||||
|
|
@ -59,9 +62,6 @@ function sessionsList(model: string, modelProvider: string) {
|
|||
|
||||
suite.define(() => {
|
||||
it("explains chat-only models and keeps model switching as the recovery path", async () => {
|
||||
if (proofDir) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { mkdir, rm } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import {
|
||||
activateChatHeaderPanelAction,
|
||||
|
|
@ -14,7 +14,6 @@ const suite = createControlUiE2eSuite({
|
|||
unavailableMessage: (executablePath) => `Playwright Chromium is unavailable at ${executablePath}`,
|
||||
});
|
||||
|
||||
const artifactDir = path.resolve(process.cwd(), ".artifacts/control-ui-e2e/chat-panel-reflow");
|
||||
const chatSessionKey = "agent:main:main";
|
||||
|
||||
const longReply =
|
||||
|
|
@ -65,8 +64,7 @@ async function expectMessagesNotToOverlap(page: import("playwright").Page): Prom
|
|||
|
||||
suite.define(() => {
|
||||
it("keeps transcript rows separate across background-task, file, and diff panel toggles", async () => {
|
||||
await rm(artifactDir, { force: true, recursive: true });
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
const artifactDir = createControlUiE2eArtifactDir("chat-panel-reflow");
|
||||
await suite.withPage(
|
||||
{
|
||||
locale: "en-US",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// Control UI tests cover session pull request chips above the chat composer.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type BrowserContext, type Page } from "playwright";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { beforeEach, afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { CONTROL_UI_SESSION_PULL_REQUESTS_CHANGED_EVENT } from "../../../src/gateway/control-ui-contract.js";
|
||||
import { SESSION_PULL_REQUESTS_SUBSCRIBE_METHOD } from "../lib/session-pull-requests.ts";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
controlUiSessionUrl,
|
||||
|
|
@ -20,18 +20,18 @@ const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
|||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const publicationProofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"github-publication",
|
||||
);
|
||||
const stackingProofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"pr-chip-stacking",
|
||||
);
|
||||
let publicationProofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
publicationProofDir = createControlUiE2eArtifactDir("github-publication");
|
||||
}
|
||||
});
|
||||
let stackingProofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
stackingProofDir = createControlUiE2eArtifactDir("pr-chip-stacking");
|
||||
}
|
||||
});
|
||||
|
||||
let server: ControlUiE2eServer;
|
||||
// Browser contexts preserve test isolation; keep one process warm for this file.
|
||||
|
|
@ -317,7 +317,6 @@ describeControlUiE2e("session pull request chips", () => {
|
|||
|
||||
await overlapLastTranscriptRowWithPullRequestChip(page);
|
||||
if (captureUiProof) {
|
||||
await mkdir(stackingProofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
path: path.join(stackingProofDir, `${label}.png`),
|
||||
|
|
@ -391,9 +390,6 @@ describeControlUiE2e("session pull request chips", () => {
|
|||
});
|
||||
|
||||
it("publishes through the Gateway and renders the terminal pull request URL", async () => {
|
||||
if (captureUiProof) {
|
||||
await mkdir(publicationProofDir, { recursive: true });
|
||||
}
|
||||
const context = await browser.newContext({
|
||||
colorScheme: "light",
|
||||
locale: "en-US",
|
||||
|
|
@ -672,7 +668,6 @@ describeControlUiE2e("session pull request chips", () => {
|
|||
)
|
||||
.toBe("https://github.com/openclaw/openclaw/pull/new/openclaw/rejected-publication");
|
||||
if (captureUiProof) {
|
||||
await mkdir(publicationProofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
@ -815,7 +810,6 @@ describeControlUiE2e("session pull request chips", () => {
|
|||
.toContain("Start a live agent turn");
|
||||
expect(await gateway.getRequests("sessions.github.publish")).toHaveLength(0);
|
||||
if (captureUiProof) {
|
||||
await mkdir(publicationProofDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
// Real-browser proof + regression for #93041: provider usage from models.authStatus remains
|
||||
// available in the desktop composer's context popover. Screenshots go to the ignored artifacts tree.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { BrowserContext, Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
controlUiE2eWaitTimeoutMs,
|
||||
controlUiSessionUrl,
|
||||
|
|
@ -17,14 +17,10 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const baseTime = 1_700_000_000_000;
|
||||
const artifactDir = path.resolve(process.cwd(), ".artifacts/control-ui-e2e/chat-quota-pill-93041");
|
||||
let artifactDir: string;
|
||||
const captureOwnershipProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const ownershipProofPhase = process.env.OPENCLAW_UI_PROOF_PHASE?.trim() || "candidate";
|
||||
const ownershipProofDir = path.resolve(
|
||||
process.cwd(),
|
||||
".artifacts/ui-visual-proof/agent-quota-ownership",
|
||||
ownershipProofPhase,
|
||||
);
|
||||
let ownershipProofDir: string;
|
||||
|
||||
const authStatusWithUsage = {
|
||||
ts: baseTime,
|
||||
|
|
@ -333,6 +329,12 @@ async function openVisibleQuotaPopover(page: Page) {
|
|||
}
|
||||
|
||||
suite.define(() => {
|
||||
beforeEach(() => {
|
||||
artifactDir = createControlUiE2eArtifactDir("chat-quota-pill-93041");
|
||||
if (captureOwnershipProof) {
|
||||
ownershipProofDir = path.join(artifactDir, "agent-quota-ownership", ownershipProofPhase);
|
||||
}
|
||||
});
|
||||
it("shows high context pressure without a compact action", async () => {
|
||||
const fixture = await openChat(authStatusWithUsage, {
|
||||
"sessions.list": highPressureSessions,
|
||||
|
|
@ -462,9 +464,6 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("binds delayed auth and quota presentation to the selected global agent", async () => {
|
||||
if (captureOwnershipProof) {
|
||||
await mkdir(ownershipProofDir, { recursive: true });
|
||||
}
|
||||
const context = await suite.browser.newContext({
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
|
|
@ -737,7 +736,7 @@ suite.define(() => {
|
|||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
path: path.join(ownershipProofDir, "03-work-settled.png"),
|
||||
path: path.join(ownershipProofDir, "04-work-quota-popover.png"),
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Locator, Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
controlUiBundledSettingsStorageKey,
|
||||
controlUiSessionUrl,
|
||||
|
|
@ -17,8 +17,22 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const sessionKey = "agent:main:rail-tabs";
|
||||
const proofDir = process.env.OPENCLAW_UI_RAIL_PROOF_DIR?.trim();
|
||||
const videoDir = process.env.OPENCLAW_UI_RAIL_VIDEO_DIR?.trim();
|
||||
const proofDirParent = process.env.OPENCLAW_UI_RAIL_PROOF_DIR?.trim();
|
||||
let proofDir: string | undefined;
|
||||
beforeEach(() => {
|
||||
proofDir = proofDirParent
|
||||
? createControlUiE2eArtifactDir("chat-rail-columns", proofDirParent)
|
||||
: undefined;
|
||||
});
|
||||
const videoDirParent = process.env.OPENCLAW_UI_RAIL_VIDEO_DIR?.trim();
|
||||
let videoDir: string | undefined;
|
||||
beforeEach(() => {
|
||||
videoDir = videoDirParent
|
||||
? proofDir && proofDirParent && path.resolve(videoDirParent) === path.resolve(proofDirParent)
|
||||
? proofDir
|
||||
: createControlUiE2eArtifactDir("chat-rail-columns", videoDirParent)
|
||||
: undefined;
|
||||
});
|
||||
|
||||
const historyMessages = Array.from({ length: 10 }, (_, index) => ({
|
||||
id: `rail-tabs-${index}`,
|
||||
|
|
@ -276,7 +290,6 @@ async function captureRichPanel(page: Page, name: string) {
|
|||
if (!proofDir) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
const clip = await page.evaluate(() => {
|
||||
const elements = [
|
||||
document.querySelector<HTMLElement>(".chat-pane__header"),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
createChatFlowE2eSuite,
|
||||
expectRequestCountStable,
|
||||
|
|
@ -31,10 +32,10 @@ suite.define(() => {
|
|||
])(
|
||||
"settles a $name without repeatedly loading the reply preview",
|
||||
async ({ artifact, response }) => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-reply-preview-recovery", artifactRoot)
|
||||
: undefined;
|
||||
const context = await suite.newBrowserContext({
|
||||
locale: "en-US",
|
||||
...(artifactDir
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
// Control UI E2E tests cover chat run lifecycle behavior through the Gateway WebSocket.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Page } from "playwright";
|
||||
import { afterEach, expect, it } from "vitest";
|
||||
|
|
@ -232,8 +231,7 @@ suite.define(() => {
|
|||
expect(await currentPage.locator(".chat-reading-indicator").count()).toBe(0);
|
||||
expect(await assistantGroup.getByText("Working…", { exact: true }).count()).toBe(1);
|
||||
|
||||
const artifactDir = path.resolve(".artifacts/control-ui-e2e/chat-single-turn-status");
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
const artifactDir = path.join(suite.artifactDir, "chat-single-turn-status");
|
||||
await currentPage.screenshot({
|
||||
path: path.join(artifactDir, "continuing-reply.png"),
|
||||
fullPage: true,
|
||||
|
|
@ -275,11 +273,11 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("restores only the unpersisted assistant response after reconnecting", async () => {
|
||||
const artifactDir = path.resolve(".artifacts/control-ui-e2e/chat-inflight-reconnect");
|
||||
const artifactDir =
|
||||
process.env.OPENCLAW_CAPTURE_UI_PROOF === "1"
|
||||
? path.join(suite.artifactDir, "chat-inflight-reconnect")
|
||||
: "";
|
||||
const captureProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
if (captureProof) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
const context = await suite.newBrowserContext({
|
||||
viewport: { height: 800, width: 1200 },
|
||||
...(captureProof
|
||||
|
|
@ -586,11 +584,11 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("renders a safe self-abort diagnostic without leaving stale composer status", async () => {
|
||||
const artifactDir = path.resolve(".artifacts/control-ui-e2e/chat-abort-diagnostic");
|
||||
const artifactDir =
|
||||
process.env.OPENCLAW_CAPTURE_UI_PROOF === "1"
|
||||
? path.join(suite.artifactDir, "chat-abort-diagnostic")
|
||||
: "";
|
||||
const captureProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
if (captureProof) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
const context = await suite.newBrowserContext({ viewport: { height: 800, width: 1200 } });
|
||||
const currentPage = await context.newPage();
|
||||
page = currentPage;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Locator, Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
controlUiSessionUrl,
|
||||
installMockGateway,
|
||||
|
|
@ -16,7 +16,13 @@ const suite = createControlUiE2eSuite({
|
|||
startServerBeforeBrowser: true,
|
||||
});
|
||||
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
let artifactDir: string | undefined;
|
||||
beforeEach(() => {
|
||||
artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-session-companion-clear", artifactRoot)
|
||||
: undefined;
|
||||
});
|
||||
const answer = "Keep this companion answer visible until the reset succeeds.";
|
||||
const initiatingSessionKey = "agent:main:companion-clear";
|
||||
const nextSessionKey = "agent:main:companion-next";
|
||||
|
|
@ -30,9 +36,6 @@ type CompanionSurface = {
|
|||
};
|
||||
|
||||
async function withCompanion(run: (surface: CompanionSurface) => Promise<void>): Promise<void> {
|
||||
if (artifactDir) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
}
|
||||
await suite.withPage(
|
||||
{
|
||||
locale: "en-US",
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// Control UI tests cover the session diff panel (sessions.diff RPC).
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type BrowserContext } from "playwright";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { beforeEach, afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { CONTROL_UI_SESSION_PULL_REQUESTS_CHANGED_EVENT } from "../../../src/gateway/control-ui-contract.js";
|
||||
import { SESSION_PULL_REQUESTS_SUBSCRIBE_METHOD } from "../lib/session-pull-requests.ts";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
controlUiBundledSettingsStorageKey,
|
||||
|
|
@ -25,7 +25,12 @@ const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
|||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const captureProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const artifactDir = path.resolve(process.cwd(), ".artifacts/control-ui-e2e/diff-highlighting");
|
||||
let artifactDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureProof) {
|
||||
artifactDir = createControlUiE2eArtifactDir("diff-highlighting");
|
||||
}
|
||||
});
|
||||
|
||||
let server: ControlUiE2eServer;
|
||||
// Browser contexts preserve test isolation; keep one process warm for this file.
|
||||
|
|
@ -607,7 +612,6 @@ describeControlUiE2e("session diff panel", () => {
|
|||
),
|
||||
).toBe(true);
|
||||
if (captureProof) {
|
||||
await mkdir(artifactDir, { recursive: true });
|
||||
await panelSurface.screenshot({ path: path.join(artifactDir, "unified-light.png") });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser, type BrowserContext } from "playwright";
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { beforeEach, afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
controlUiSessionUrl,
|
||||
|
|
@ -17,12 +17,12 @@ const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
|||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
const captureUiProof = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const proofDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"session-discussion-toggle",
|
||||
);
|
||||
let proofDir: string;
|
||||
beforeEach(() => {
|
||||
if (captureUiProof) {
|
||||
proofDir = createControlUiE2eArtifactDir("session-discussion-toggle");
|
||||
}
|
||||
});
|
||||
|
||||
let server: ControlUiE2eServer;
|
||||
let browser: Browser;
|
||||
|
|
@ -41,9 +41,6 @@ describeControlUiE2e("session discussion toggle", () => {
|
|||
}
|
||||
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
server = await startControlUiE2eServer();
|
||||
if (captureUiProof) {
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterEach(closeOpenContexts);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { Locator, Page } from "playwright";
|
||||
import { expect, it } from "vitest";
|
||||
import { beforeEach, expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import {
|
||||
controlUiBundledSettingsStorageKey,
|
||||
installMockGateway,
|
||||
|
|
@ -19,7 +19,13 @@ const suite = createControlUiE2eSuite({
|
|||
});
|
||||
|
||||
const sessionKey = "agent:main:side-panel-clearance";
|
||||
const proofDir = process.env.OPENCLAW_UI_RAIL_PROOF_DIR?.trim();
|
||||
const proofDirParent = process.env.OPENCLAW_UI_RAIL_PROOF_DIR?.trim();
|
||||
let proofDir: string | undefined;
|
||||
beforeEach(() => {
|
||||
proofDir = proofDirParent
|
||||
? createControlUiE2eArtifactDir("chat-side-panel-clearance", proofDirParent)
|
||||
: undefined;
|
||||
});
|
||||
const limitedScopes = ["operator.read", "operator.write"];
|
||||
const historyMessages = [
|
||||
{
|
||||
|
|
@ -196,7 +202,6 @@ async function capturePanel(page: Page, name: string): Promise<void> {
|
|||
if (!proofDir) {
|
||||
return;
|
||||
}
|
||||
await mkdir(proofDir, { recursive: true });
|
||||
await page.screenshot({ fullPage: true, path: path.join(proofDir, `${name}.png`) });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Control UI E2E tests cover composable skill references in the chat composer.
|
||||
import path from "node:path";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -10,7 +11,10 @@ const suite = createControlUiE2eSuite({
|
|||
|
||||
suite.define(() => {
|
||||
it("references multiple skills inside a normal prompt and sends their raw names", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-skill-references", artifactRoot)
|
||||
: undefined;
|
||||
await suite.withPage(
|
||||
{
|
||||
viewport: { width: 1280, height: 900 },
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import path from "node:path";
|
||||
import { text } from "node:stream/consumers";
|
||||
import { expect, it } from "vitest";
|
||||
import { createControlUiE2eArtifactDir } from "../test-helpers/control-ui-e2e-artifacts.ts";
|
||||
import { installMockGateway } from "../test-helpers/control-ui-e2e.ts";
|
||||
import { createControlUiE2eSuite } from "./control-ui-e2e-suite.test-support.ts";
|
||||
|
||||
|
|
@ -13,7 +14,10 @@ suite.define(() => {
|
|||
it.each(["/export-session", "/export"])(
|
||||
"shows an empty export result and retains staged attachments for %s",
|
||||
async (command) => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-slash-command-ranking", artifactRoot)
|
||||
: undefined;
|
||||
await suite.withPage({ viewport: { width: 1280, height: 900 } }, async ({ page }) => {
|
||||
const gateway = await installMockGateway(page, { historyMessages: [] });
|
||||
const downloads: string[] = [];
|
||||
|
|
@ -99,7 +103,10 @@ suite.define(() => {
|
|||
});
|
||||
|
||||
it("keeps visible search results and keyboard selection in relevance order", async () => {
|
||||
const artifactDir = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactRoot = process.env.OPENCLAW_UI_E2E_ARTIFACT_DIR?.trim();
|
||||
const artifactDir = artifactRoot
|
||||
? createControlUiE2eArtifactDir("chat-slash-command-ranking", artifactRoot)
|
||||
: undefined;
|
||||
await suite.withPage(
|
||||
{
|
||||
viewport: { width: 1280, height: 900 },
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue