Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ jobs:
- name: Lint
run: yarn lint

- run: yarn test unitTests
- name: Unit tests
run: yarn test --testPathIgnorePatterns integrationTests

integration:
needs: check
Expand All @@ -47,4 +48,4 @@ jobs:
- run: yarn install
- run: yarn run build
- name: Test
run: yarn test -t "${{ matrix.browser }} Integration Test"
run: yarn test integrationTests -t "${{ matrix.browser }} Integration Test"
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ All notable changes to the full browser extension will be documented in this fil

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased
### Added
- Script Viewer, opened from the new "View Script" button, which allows a recorded script to be viewed, edited
(add, move, edit, delete), replayed in a new window or tab (with optional step through), loaded and saved.

## 0.2.4 - 2026-10-08
### Fixed
- Record text fields updated via JS with no associated events.
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.rec.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,12 @@ All notable changes to the recorder browser extension will be documented in this

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## Unreleased
### Added
- Script Viewer, opened from the new "View Script" button, which allows a recorded script to be viewed, edited
(add, move, edit, delete), replayed in a new window or tab (with optional step through), loaded and saved.
- Configuration page, opened from the popup, for the options that apply to the recorder.

## 0.2.4 - 2026-10-08
### Fixed
- Record text fields updated via JS with no associated events.
Expand Down
81 changes: 57 additions & 24 deletions source/Background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
REPORT_OBJECT,
RESET_ZEST_SCRIPT,
SESSION_STORAGE,
SET_ZEST_STATEMENTS,
STOP_RECORDING,
ZEST_SCRIPT,
} from '../utils/constants';
Expand Down Expand Up @@ -179,6 +180,27 @@ function sendZestScriptToZAP(
}
}

/*
Everything that changes the script goes through this queue, so that each change and its write to storage finish before the next one starts.
Otherwise, for example, saving edited statements could overwrite statements recorded while the save was being stored.
*/
let scriptChanges: Promise<unknown> = Promise.resolve();

function inOrder<T>(change: () => Promise<T> | T): Promise<T> {
const result = scriptChanges.then(change);
scriptChanges = result.catch(() => undefined);
return result;
}

function storeScript(): Promise<void> {
return Browser.storage.local
.set({
zestStatements: zestScript.getStatements(),
zestCurIndex: zestScript.getCurIndex(),
})
.catch((error) => console.error(`Failed to store the script: ${error}`));
}

async function handleMessage(
request: MessageEvent,
zapurl: string,
Expand Down Expand Up @@ -231,41 +253,52 @@ async function handleMessage(
break;
}

case ZEST_SCRIPT: {
const data = zestScript.addStatement(request.data);
sendZestScriptToZAP(data, zapkey, zapurl);
Browser.storage.local.set({
zestStatements: zestScript.getStatements(),
zestCurIndex: zestScript.getCurIndex(),
case ZEST_SCRIPT:
await inOrder(async () => {
const data = zestScript.addStatement(request.data);
sendZestScriptToZAP(data, zapkey, zapurl);
await storeScript();
});
break;
}

case GET_ZEST_SCRIPT:
return zestScript.getZestScript();
return inOrder(() => zestScript.getZestScript());
Comment thread
psiinon marked this conversation as resolved.

case SET_ZEST_STATEMENTS:
await inOrder(async () => {
// Only change the script once the new one has been stored, so that if
// storing fails the script and what is stored stay the same
const prepared = zestScript.prepareStatements(request.data);
await Browser.storage.local.set({
zestStatements: prepared.statements,
zestCurIndex: prepared.curIndex,
});
zestScript.restoreStatements(prepared.statements, prepared.curIndex);
});
break;

case RESET_ZEST_SCRIPT:
zestScript.reset();
Browser.storage.local.set({zestStatements: [], zestCurIndex: 1});
await inOrder(async () => {
zestScript.reset();
await storeScript();
});
break;

case STOP_RECORDING: {
if (zestScript.getZestStatementCount() > 0) {
const {zapclosewindowhandle} = await Browser.storage.sync.get({
zapclosewindowhandle: false,
});
if (zapclosewindowhandle) {
const stmt = new ZestStatementWindowClose(0);
const data = zestScript.addStatement(stmt.toJSON());
sendZestScriptToZAP(data, zapkey, zapurl);
Browser.storage.local.set({
zestStatements: zestScript.getStatements(),
zestCurIndex: zestScript.getCurIndex(),
case STOP_RECORDING:
await inOrder(async () => {
if (zestScript.getZestStatementCount() > 0) {
const {zapclosewindowhandle} = await Browser.storage.sync.get({
zapclosewindowhandle: false,
});
if (zapclosewindowhandle) {
const stmt = new ZestStatementWindowClose(0);
const data = zestScript.addStatement(stmt.toJSON());
sendZestScriptToZAP(data, zapkey, zapurl);
await storeScript();
}
}
}
});
break;
}

default:
// Handle unknown request type
Expand Down
29 changes: 28 additions & 1 deletion source/ContentScript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
InteractableState,
} from '../types/ReportedModel';
import Recorder from './recorder';
import Replayer from './replayer';
import {
hasPointerStyle,
getInteractableState,
Expand All @@ -40,6 +41,11 @@ import {
LOCAL_ZAP_ENABLE,
LOCAL_ZAP_RECORD,
REPORT_EVENT,
REPLAY_ABORT,
REPLAY_FINISHED,
REPLAY_PAUSE,
REPLAY_PING,
REPLAY_STATEMENT,
REPORT_OBJECT,
SESSION_STORAGE,
URL_ZAP_ENABLE,
Expand All @@ -58,6 +64,8 @@ let trackedElementRefs: WeakRef<Element>[] = [];

const recorder = new Recorder();

const replayer = new Replayer();

function reportStorage(
name: string,
storage: Storage,
Expand Down Expand Up @@ -429,7 +437,26 @@ Browser.runtime.onMessage.addListener(
message: any,
_sender: Runtime.MessageSender,
_sendResponse: (response?: any) => void
) => {
): any => {
if (message.type === REPLAY_STATEMENT) {
return replayer.execute(message.data, message.display);
}
if (message.type === REPLAY_PING) {
// Lets the player know that the page is ready to be sent statements
return Promise.resolve(true);
}
if (message.type === REPLAY_ABORT) {
replayer.abort();
return Promise.resolve(true);
}
if (message.type === REPLAY_PAUSE) {
replayer.pause(message.data);
return Promise.resolve(true);
}
if (message.type === REPLAY_FINISHED) {
replayer.finish(message.data);
return Promise.resolve(true);
}
if (message.type === ZAP_START_RECORDING) {
configureExtension();
recorder.recordUserInteractions();
Expand Down
Loading
Loading