Skip to content

Commit ad7ff03

Browse files
committed
refactor: Separate session steps (in JSON) and code generation (in JS) in Resources
1 parent 9cebdbd commit ad7ff03

3 files changed

Lines changed: 129 additions & 78 deletions

File tree

src/recording/resources.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,48 +8,48 @@ function getCurrentSessionId(): string | null {
88
return (getBrowser as any).__state?.currentSession ?? null;
99
}
1010

11+
export interface SessionStepsPayload {
12+
stepsJson: string;
13+
generatedJs: string;
14+
}
15+
1116
export function buildSessionsIndex(): string {
1217
const histories = getSessionHistory();
13-
if (histories.size === 0) return 'No sessions recorded.';
14-
1518
const currentId = getCurrentSessionId();
16-
const lines = [`Sessions (${histories.size} total):\n`];
17-
for (const [id, h] of histories) {
18-
const ended = h.endedAt ?? '-';
19-
const current = id === currentId ? ' [current]' : '';
20-
lines.push(`- ${id} ${h.type} started: ${h.startedAt} ended: ${ended} ${h.steps.length} steps${current}`);
21-
}
22-
return lines.join('\n');
19+
const sessions = Array.from(histories.values()).map((h) => ({
20+
sessionId: h.sessionId,
21+
type: h.type,
22+
startedAt: h.startedAt,
23+
...(h.endedAt ? { endedAt: h.endedAt } : {}),
24+
stepCount: h.steps.length,
25+
isCurrent: h.sessionId === currentId,
26+
}));
27+
return JSON.stringify({ sessions });
2328
}
2429

25-
export function buildCurrentSessionSteps(): string {
30+
export function buildCurrentSessionSteps(): SessionStepsPayload | null {
2631
const currentId = getCurrentSessionId();
27-
if (!currentId) return 'No active session.';
32+
if (!currentId) return null;
33+
2834
return buildSessionStepsById(currentId);
2935
}
3036

31-
export function buildSessionStepsById(sessionId: string): string {
37+
export function buildSessionStepsById(sessionId: string): SessionStepsPayload | null {
3238
const history = getSessionHistory().get(sessionId);
33-
if (!history) return `Session not found: ${sessionId}`;
34-
return formatSessionSteps(history);
39+
if (!history) return null;
40+
41+
return buildSessionPayload(history);
3542
}
3643

37-
function formatSessionSteps(history: SessionHistory): string {
38-
const header = `Session: ${history.sessionId} (${history.type}) — ${history.steps.length} steps\n`;
39-
40-
const stepLines = history.steps.map((step) => {
41-
if (step.tool === '__session_transition__') {
42-
return `--- session transitioned to ${step.params.newSessionId ?? 'unknown'} at ${step.timestamp} ---`;
43-
}
44-
const statusLabel = step.status === 'ok' ? '[ok] ' : '[error]';
45-
const params = Object.entries(step.params)
46-
.map(([k, v]) => `${k}="${v}"`)
47-
.join(' ');
48-
const errorSuffix = step.error ? ` — ${step.error}` : '';
49-
return `${step.index}. ${statusLabel} ${step.tool.padEnd(24)} ${params}${errorSuffix} ${step.durationMs}ms`;
44+
function buildSessionPayload(history: SessionHistory): SessionStepsPayload {
45+
const stepsJson = JSON.stringify({
46+
sessionId: history.sessionId,
47+
type: history.type,
48+
startedAt: history.startedAt,
49+
...(history.endedAt ? { endedAt: history.endedAt } : {}),
50+
stepCount: history.steps.length,
51+
steps: history.steps,
5052
});
5153

52-
const stepsText = stepLines.length > 0 ? stepLines.join('\n') : '(no steps yet)';
53-
const jsCode = generateCode(history);
54-
return `${header}\nSteps:\n${stepsText}\n\n--- Generated WebdriverIO JS ---\n${jsCode}`;
54+
return { stepsJson, generatedJs: generateCode(history) };
5555
}

src/server.ts

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -146,32 +146,58 @@ registerTool(executeScriptToolDefinition, executeScriptTool);
146146
server.registerResource(
147147
'sessions',
148148
'wdio://sessions',
149-
{ description: 'Index of all browser and app sessions with step counts' },
149+
{ description: 'JSON index of all browser and app sessions with metadata and step counts' },
150150
async () => ({
151-
contents: [{ uri: 'wdio://sessions', mimeType: 'text/plain', text: buildSessionsIndex() }],
151+
contents: [{ uri: 'wdio://sessions', mimeType: 'application/json', text: buildSessionsIndex() }],
152152
}),
153153
);
154154

155155
server.registerResource(
156156
'session-current-steps',
157157
'wdio://session/current/steps',
158-
{ description: 'Steps for the currently active session with generated WebdriverIO JS' },
159-
async () => ({
160-
contents: [{ uri: 'wdio://session/current/steps', mimeType: 'text/plain', text: buildCurrentSessionSteps() }],
161-
}),
158+
{ description: 'JSON step log for the currently active session' },
159+
async () => {
160+
const payload = buildCurrentSessionSteps();
161+
return {
162+
contents: [{ uri: 'wdio://session/current/steps', mimeType: 'application/json', text: payload?.stepsJson ?? '{"error":"No active session"}' }],
163+
};
164+
},
165+
);
166+
167+
server.registerResource(
168+
'session-current-code',
169+
'wdio://session/current/code',
170+
{ description: 'Generated WebdriverIO JS code for the currently active session' },
171+
async () => {
172+
const payload = buildCurrentSessionSteps();
173+
return {
174+
contents: [{ uri: 'wdio://session/current/code', mimeType: 'text/plain', text: payload?.generatedJs ?? '// No active session' }],
175+
};
176+
},
162177
);
163178

164179
server.registerResource(
165180
'session-steps',
166181
new ResourceTemplate('wdio://session/{sessionId}/steps', { list: undefined }),
167-
{ description: 'Steps for a specific session by ID with generated WebdriverIO JS' },
168-
async (uri, { sessionId }) => ({
169-
contents: [{
170-
uri: uri.href,
171-
mimeType: 'text/plain',
172-
text: buildSessionStepsById(sessionId as string),
173-
}],
174-
}),
182+
{ description: 'JSON step log for a specific session by ID' },
183+
async (uri, { sessionId }) => {
184+
const payload = buildSessionStepsById(sessionId as string);
185+
return {
186+
contents: [{ uri: uri.href, mimeType: 'application/json', text: payload?.stepsJson ?? `{"error":"Session not found: ${sessionId}"}` }],
187+
};
188+
},
189+
);
190+
191+
server.registerResource(
192+
'session-code',
193+
new ResourceTemplate('wdio://session/{sessionId}/code', { list: undefined }),
194+
{ description: 'Generated WebdriverIO JS code for a specific session by ID' },
195+
async (uri, { sessionId }) => {
196+
const payload = buildSessionStepsById(sessionId as string);
197+
return {
198+
contents: [{ uri: uri.href, mimeType: 'text/plain', text: payload?.generatedJs ?? `// Session not found: ${sessionId}` }],
199+
};
200+
},
175201
);
176202

177203
async function main() {

tests/recording/resources.test.ts

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -31,75 +31,100 @@ beforeEach(() => {
3131
});
3232

3333
describe('buildSessionsIndex', () => {
34-
it('returns "No sessions recorded." when empty', () => {
35-
expect(buildSessionsIndex()).toBe('No sessions recorded.');
34+
it('returns valid JSON with empty sessions array when no sessions', () => {
35+
const parsed = JSON.parse(buildSessionsIndex());
36+
expect(parsed.sessions).toEqual([]);
3637
});
3738

38-
it('lists all sessions with id, type, step count', () => {
39+
it('lists all sessions with id, type, stepCount', () => {
3940
addHistory('abc-1', 'browser', false, true);
4041
addHistory('def-2', 'ios');
41-
const result = buildSessionsIndex();
42-
expect(result).toContain('abc-1');
43-
expect(result).toContain('def-2');
44-
expect(result).toContain('browser');
45-
expect(result).toContain('ios');
42+
const parsed = JSON.parse(buildSessionsIndex());
43+
expect(parsed.sessions).toHaveLength(2);
44+
const ids = parsed.sessions.map((s: any) => s.sessionId);
45+
expect(ids).toContain('abc-1');
46+
expect(ids).toContain('def-2');
4647
});
4748

48-
it('marks current session with [current]', () => {
49+
it('marks the current session with isCurrent: true', () => {
4950
addHistory('cur-1', 'browser', true);
5051
addHistory('old-1', 'browser', false, true);
51-
const result = buildSessionsIndex();
52-
expect(result).toContain('[current]');
53-
// only the current one should be marked
54-
const lines = result.split('\n').filter((l) => l.includes('[current]'));
55-
expect(lines).toHaveLength(1);
56-
expect(lines[0]).toContain('cur-1');
52+
const parsed = JSON.parse(buildSessionsIndex());
53+
const current = parsed.sessions.filter((s: any) => s.isCurrent);
54+
expect(current).toHaveLength(1);
55+
expect(current[0].sessionId).toBe('cur-1');
5756
});
5857

59-
it('shows step count per session', () => {
58+
it('includes stepCount per session', () => {
6059
const h = addHistory('sess-steps', 'browser');
6160
h.steps.push({ index: 1, tool: 'navigate', params: {}, status: 'ok', durationMs: 10, timestamp: '' });
6261
h.steps.push({ index: 2, tool: 'click_element', params: {}, status: 'ok', durationMs: 5, timestamp: '' });
63-
const result = buildSessionsIndex();
64-
expect(result).toContain('2 steps');
62+
const parsed = JSON.parse(buildSessionsIndex());
63+
expect(parsed.sessions[0].stepCount).toBe(2);
6564
});
6665
});
6766

6867
describe('buildCurrentSessionSteps', () => {
69-
it('returns "No active session." when no current session', () => {
70-
expect(buildCurrentSessionSteps()).toBe('No active session.');
68+
it('returns null when no current session', () => {
69+
expect(buildCurrentSessionSteps()).toBeNull();
7170
});
7271

73-
it('returns step listing and generated JS for current session', () => {
72+
it('returns stepsJson and generatedJs for the current session', () => {
7473
const h = addHistory('live-1', 'browser', true);
7574
h.steps.push({ index: 1, tool: 'navigate', params: { url: 'https://x.com' }, status: 'ok', durationMs: 50, timestamp: '2026-01-01T00:00:00.000Z' });
7675
const result = buildCurrentSessionSteps();
77-
expect(result).toContain('live-1');
78-
expect(result).toContain('navigate');
79-
expect(result).toContain('Generated WebdriverIO JS');
80-
expect(result).toContain("await browser.url('https://x.com');");
76+
expect(result).not.toBeNull();
77+
expect(result!.stepsJson).toBeDefined();
78+
expect(result!.generatedJs).toBeDefined();
8179
});
8280
});
8381

8482
describe('buildSessionStepsById', () => {
85-
it('returns "Session not found" for unknown sessionId', () => {
86-
expect(buildSessionStepsById('nonexistent')).toBe('Session not found: nonexistent');
83+
it('returns null for unknown sessionId', () => {
84+
expect(buildSessionStepsById('nonexistent')).toBeNull();
8785
});
8886

89-
it('returns session steps for known sessionId', () => {
87+
it('stepsJson is valid JSON with session metadata and steps array', () => {
9088
const h = addHistory('hist-1', 'ios');
9189
h.steps.push({ index: 1, tool: 'tap_element', params: { selector: '~btn' }, status: 'ok', durationMs: 20, timestamp: '2026-01-01T00:00:00.000Z' });
9290
const result = buildSessionStepsById('hist-1');
93-
expect(result).toContain('hist-1');
94-
expect(result).toContain('tap_element');
95-
expect(result).toContain('Generated WebdriverIO JS');
91+
expect(result).not.toBeNull();
92+
const parsed = JSON.parse(result!.stepsJson);
93+
expect(parsed.sessionId).toBe('hist-1');
94+
expect(parsed.type).toBe('ios');
95+
expect(parsed.startedAt).toBe('2026-01-01T00:00:00.000Z');
96+
expect(parsed.stepCount).toBe(1);
97+
expect(Array.isArray(parsed.steps)).toBe(true);
98+
expect(parsed.steps[0]).toMatchObject({
99+
index: 1,
100+
tool: 'tap_element',
101+
params: { selector: '~btn' },
102+
status: 'ok',
103+
durationMs: 20,
104+
});
96105
});
97106

98-
it('marks error steps with [error] in the step list', () => {
107+
it('generatedJs contains valid WebdriverIO code', () => {
108+
const h = addHistory('hist-2', 'browser');
109+
h.steps.push({ index: 1, tool: 'navigate', params: { url: 'https://example.com' }, status: 'ok', durationMs: 10, timestamp: '2026-01-01T00:00:00.000Z' });
110+
const result = buildSessionStepsById('hist-2');
111+
expect(result!.generatedJs).toContain("await browser.url('https://example.com');");
112+
expect(result!.generatedJs).toContain("import { remote } from 'webdriverio';");
113+
});
114+
115+
it('error steps appear in stepsJson with status and error fields', () => {
99116
const h = addHistory('err-1', 'browser');
100117
h.steps.push({ index: 1, tool: 'click_element', params: { selector: '#x' }, status: 'error', error: 'Not found', durationMs: 5, timestamp: '2026-01-01T00:00:00.000Z' });
101118
const result = buildSessionStepsById('err-1');
102-
expect(result).toContain('[error]');
103-
expect(result).toContain('Not found');
119+
const parsed = JSON.parse(result!.stepsJson);
120+
expect(parsed.steps[0].status).toBe('error');
121+
expect(parsed.steps[0].error).toBe('Not found');
122+
});
123+
124+
it('stepsJson includes endedAt when session has ended', () => {
125+
addHistory('ended-1', 'browser', false, true);
126+
const result = buildSessionStepsById('ended-1');
127+
const parsed = JSON.parse(result!.stepsJson);
128+
expect(parsed.endedAt).toBe('2026-01-01T01:00:00.000Z');
104129
});
105130
});

0 commit comments

Comments
 (0)