diff --git a/client/src/pages/run/RunDetailPage.tsx b/client/src/pages/run/RunDetailPage.tsx
index 2e8abf3..02370ba 100644
--- a/client/src/pages/run/RunDetailPage.tsx
+++ b/client/src/pages/run/RunDetailPage.tsx
@@ -291,7 +291,7 @@ export function RunDetailPage() {
items={[
{ label: t('scenarios.title'), icon: , onClick: () => navigate('/scenarios') },
{
- label: scenario?.name ?? `#${id}`,
+ label: scenario?.name ?? `${id}`,
onClick: () => navigate(`/scenarios/${id}`),
},
{
@@ -299,7 +299,7 @@ export function RunDetailPage() {
icon: ,
onClick: () => navigate(`/scenarios/${id}/runs`),
},
- { label: `#${runId}` },
+ { label: `${runId}` },
]}
/>
@@ -312,27 +312,28 @@ export function RunDetailPage() {
},
- {
- term: t('runs.field_status'),
- detail: (
-
- {RUN_STATUS_LABEL[run.status]}
-
- ),
- },
- { term: t('runs.field_created'), detail: },
- { term: t('runs.field_updated'), detail: },
- ...(run.environment ? [{
- term: 'Environment',
- detail: {run.environment.name},
- }] : []),
- ...(run.session ? [{
- term: 'Session',
- detail: {run.session.sessionName},
- }] : []),
- ]}
+ items={(() => {
+ const items = [
+ { term: t('runs.field_id'), detail: },
+ {
+ term: t('runs.field_status'),
+ detail: (
+
+ {RUN_STATUS_LABEL[run.status]}
+
+ ),
+ },
+ { term: t('runs.field_created'), detail: },
+ { term: t('runs.field_updated'), detail: },
+ ];
+ if (run.environment) {
+ items.push({ term: 'Environment', detail: {run.environment.name} });
+ }
+ if (run.session) {
+ items.push({ term: 'Session', detail: {run.session.sessionName} });
+ }
+ return items;
+ })()}
/>
diff --git a/client/src/pages/scenario/ScenarioDetailPage.tsx b/client/src/pages/scenario/ScenarioDetailPage.tsx
index 4ae5e30..b606b3f 100644
--- a/client/src/pages/scenario/ScenarioDetailPage.tsx
+++ b/client/src/pages/scenario/ScenarioDetailPage.tsx
@@ -559,7 +559,6 @@ export function ScenarioDetailPage() {
type="checkbox"
checked={saveSessionFlag}
onChange={(e) => setSaveSessionFlag(e.target.checked)}
- disabled
/>
Save session
diff --git a/client/src/pages/scenario/ScenariosPage.tsx b/client/src/pages/scenario/ScenariosPage.tsx
index 0ba756f..57d3e7e 100644
--- a/client/src/pages/scenario/ScenariosPage.tsx
+++ b/client/src/pages/scenario/ScenariosPage.tsx
@@ -222,7 +222,6 @@ export function ScenariosPage() {
type="checkbox"
checked={saveSessionFlag}
onChange={(e) => setSaveSessionFlag(e.target.checked)}
- disabled
/>
Save session
diff --git a/server/src/scenario/scenario-run.entity.ts b/server/src/scenario/scenario-run.entity.ts
index 6c4895b..632f55c 100644
--- a/server/src/scenario/scenario-run.entity.ts
+++ b/server/src/scenario/scenario-run.entity.ts
@@ -8,10 +8,10 @@ import {
PrimaryGeneratedColumn,
UpdateDateColumn,
} from "typeorm";
-import { ScenarioRunStepEntity } from "./scenario-run-step.entity";
-import { ScenarioEntity } from "./scenario.entity";
import { EnvironmentEntity } from "../environment/environment.entity";
import { SessionEntity } from "../session/session.entity";
+import { ScenarioRunStepEntity } from "./scenario-run-step.entity";
+import { ScenarioEntity } from "./scenario.entity";
export type RunStatus = "pending" | "in_progress" | "pass" | "fail";
@@ -23,8 +23,8 @@ export class ScenarioRunEntity {
@Column("text")
scenarioId: string;
- @Column("text", { default: "" })
- environmentId: string;
+ @Column("text", { nullable: true, default: null })
+ environmentId: string | null;
@ManyToOne(() => ScenarioEntity, { onDelete: "CASCADE" })
@JoinColumn({ name: "scenarioId" })
diff --git a/server/src/scenario/scenario-scheduler.service.ts b/server/src/scenario/scenario-scheduler.service.ts
index c687c45..15002f6 100644
--- a/server/src/scenario/scenario-scheduler.service.ts
+++ b/server/src/scenario/scenario-scheduler.service.ts
@@ -7,6 +7,7 @@ import { chromium } from "playwright";
import { Repository } from "typeorm";
import type { ScriptLogger } from "../code-executor/code-executor.service";
import { CodeExecutorService } from "../code-executor/code-executor.service";
+import { ExecContextBuilder } from "../code-executor/exec-context.builder";
import { traceStorage } from "../common/trace-context";
import { TraceLogger } from "../common/trace-logger";
import { EnvironmentData, EnvironmentEntity } from "../environment/environment.entity";
@@ -170,7 +171,9 @@ export class ScenarioSchedulerService {
}
const handle = this.runBrowsers.get(runId);
- if (!handle) return;
+ if (!handle) {
+ return;
+ }
const sessionName = `run-${runId}`;
try {
@@ -184,7 +187,8 @@ export class ScenarioSchedulerService {
cookies.find((c) => c.name === "token")?.value ??
"";
- await this.sessionService.upsert(sessionName, token, cookies, localStorage);
+ const session = await this.sessionService.upsert(sessionName, token, cookies, localStorage);
+ await this.runRepo.update(runId, { sessionId: session.id });
this.sessionContextService.register(
sessionName,
handle.browser,
@@ -194,7 +198,7 @@ export class ScenarioSchedulerService {
// Remove from runBrowsers so closeBrowserHandle won't close it
this.runBrowsers.delete(runId);
this.logger.log(
- `Run #${runId}: browser preserved as session "${sessionName}"`,
+ `Run #${runId}: browser preserved as session "${sessionName}" (${session.id})`,
);
} catch (err) {
this.logger.warn(
@@ -223,16 +227,17 @@ export class ScenarioSchedulerService {
const creds = this.runCredentials.get(stepRun.runId);
const snips = this.runSnippets.get(stepRun.runId);
const env = this.runEnvironments.get(stepRun.runId);
- const { result: execOutput } = await this.codeExecutor.execute({
- page,
- browser: context,
- code: step.execCode,
- log: this.stepLogger(stepRun.id, stepRun.runId),
- getStepOutput,
- credentials: creds,
- environment: env,
- snippets: snips,
- });
+ const execCtx = new ExecContextBuilder()
+ .page(page)
+ .browser(context)
+ .code(step.execCode)
+ .log(this.stepLogger(stepRun.id, stepRun.runId))
+ .getStepOutput(getStepOutput)
+ .credentials(creds)
+ .environment(env)
+ .snippets(snips)
+ .build();
+ const { result: execOutput } = await this.codeExecutor.execute(execCtx);
await this.passStepRun(stepRun, null, execOutput);
} catch (err) {
@@ -310,7 +315,8 @@ export class ScenarioSchedulerService {
],
});
if (remaining === 0) {
- await this.closeBrowserHandle(stepRun.runId);
+ // Don't close the browser here — maybePreserveSession will handle it
+ // (either preserving it if saveSession=true, or closing if false)
await this.runRepo.update(stepRun.runId, { status: "pass" });
this.logger.log(`Run #${stepRun.runId} → pass (all steps passed)`);
}
diff --git a/server/src/scenario/scenario.service.ts b/server/src/scenario/scenario.service.ts
index e7275eb..b84ca20 100644
--- a/server/src/scenario/scenario.service.ts
+++ b/server/src/scenario/scenario.service.ts
@@ -322,7 +322,7 @@ export class ScenarioService {
await this.findOne(scenarioId); // 404 guard
const run = await this.runRepo.findOne({
where: { id: runId, scenarioId },
- relations: ["stepRuns", "stepRuns.scenarioStep"],
+ relations: ["stepRuns", "stepRuns.scenarioStep", "environment", "session"],
order: { stepRuns: { order: "ASC" } },
});
if (!run)
@@ -387,7 +387,7 @@ export class ScenarioService {
return this.runRepo.findOne({
where: { id: run.id },
- relations: ["stepRuns"],
+ relations: ["stepRuns", "environment", "session"],
order: { stepRuns: { order: "ASC" } },
}) as Promise;
}