Repository: DevExpress/testcafe (js)

Issue:
A non-informative report is generated for a failed assertion when running the *.testcafe fixture file using the CLI
### What is your Scenario?

Need to find out which assertion of the recorded (*.testcafe) test fails.

### What is the Current behavior?

If some action fails, the report shows an incorrect selector:
![image](https://user-images.githubusercontent.com/24777868/173788578-4154f458-37d7-4c1d-ace4-de7a458f50c5.png)

However, when an assertion fails, no selector info is shown:
![image](https://user-images.githubusercontent.com/24777868/173788791-1b9ee5dd-033b-4cdd-b7f5-a68a6766b141.png)


### What is the Expected behavior?

It should show verbose info of the failed assertion.

### What is your public website URL? (or attach your complete example)

See the attachment

### What is your TestCafe test code?

[recorded-examples.zip](https://github.com/DevExpress/testcafe/files/8907776/recorded-examples.zip)


### Your complete configuration file

_No response_

### Your complete test report

_No response_

### Screenshots

_No response_

### Steps to Reproduce

1.
2.
3.


### TestCafe version

1.19.0

### Node.js version

16.14.2

### Command-line arguments

npx testcafe chrome recorded-examples.testcafe

### Browser name(s) and version(s)

_No response_

### Platform(s) and version(s)

_No response_

### Other

_No response_

Pull request:
fix: create correct raw-callsite for hooks 
<!--
Thank you for your contribution.

Before making a PR, please read our contributing guidelines at
https://github.com/DevExpress/testcafe/blob/master/CONTRIBUTING.md#code-contribution

We recommend creating a *draft* PR, so that you can mark it as 'ready for review' when you are done.
-->

[closes #7089]

## Purpose
`actionId` for hooks in raw-tests has a prefix, that is why it is impossible to parse it as Integer and impossible to create a callsite. It needs to use `actionId` without parsing.

## Approach
1. Add test for error in hooks
2. Fix using actionId

## References
https://github.com/DevExpress/testcafe/issues/7089

## Pre-Merge TODO
- [X] Write tests for your proposed changes
- [x] Make sure that existing tests do not fail


Patch:
diff --git a/package.json b/package.json
index ed1798a73..69555addc 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
   "name": "testcafe",
   "description": "Automated browser testing for the modern web development stack.",
   "license": "MIT",
-  "version": "2.0.1-alpha.1",
+  "version": "2.0.1-alpha.2",
   "author": {
     "name": "Developer Express Inc.",
     "url": "https://www.devexpress.com/"
diff --git a/src/screenshots/capturer.js b/src/screenshots/capturer.js
index 8a36ffe20..6de588496 100644
--- a/src/screenshots/capturer.js
+++ b/src/screenshots/capturer.js
@@ -5,7 +5,11 @@ import {
 } from 'path';
 
 import { generateThumbnail } from 'testcafe-browser-tools';
-import { cropScreenshot } from './crop';
+import {
+    cropScreenshot,
+    calculateMarkPosition,
+    markSeedToId,
+} from './crop';
 import { isInQueue, addToQueue } from '../utils/async-queue';
 import WARNING_MESSAGE from '../notifications/warning-message';
 import escapeUserAgent from '../utils/escape-user-agent';
@@ -156,8 +160,13 @@ export default class Capturer {
 
             const image = await readPngFile(tempPath);
 
+            const markSeedPosition = markSeed ? calculateMarkPosition(image, markSeed) : null;
+
+            if (markSeed && !markSeedPosition)
+                this.warningLog.addWarning(WARNING_MESSAGE.screenshotMarkNotFound, tempPath, markSeedToId(markSeed));
+
             const croppedImage = await cropScreenshot(image, {
-                markSeed,
+                markSeedPosition,
                 clientAreaDimensions,
                 path:           tempPath,
                 cropDimensions: Capturer._getCropDimensions(cropDimensions, pageDimensions),
diff --git a/src/screenshots/crop.js b/src/screenshots/crop.js
index bffb7edc6..2182f495b 100644
--- a/src/screenshots/crop.js
+++ b/src/screenshots/crop.js
@@ -1,6 +1,5 @@
 import { copyImagePart } from './utils';
 import limitNumber from '../utils/limit-number';
-import renderTemplate from '../utils/render-template';
 import { deleteFile } from '../utils/promisified-functions';
 import { InvalidElementScreenshotDimensionsError } from '../errors/test-run/';
 import {
@@ -9,13 +8,11 @@ import {
     MARK_BYTES_PER_PIXEL,
 } from './constants';
 
-import WARNING_MESSAGES from '../notifications/warning-message';
-
 const MARK_SEED_ERROR_THRESHOLD = 10;
 const WHITE_COLOR_PART          = 255;
 const BLACK_COLOR_PART          = 0;
 
-function markSeedToId (markSeed) {
+export function markSeedToId (markSeed) {
     let id = 0;
 
     for (let i = 0; i < MARK_LENGTH; i++)
@@ -102,7 +99,7 @@ export function getClipInfoByCropDimensions ({ clipRight, clipLeft, clipBottom,
     };
 }
 
-export function calculateClipInfo (pngImage, path, markSeed, clientAreaDimensions, cropDimensions) {
+export function calculateClipInfo (pngImage, markSeedPosition, clientAreaDimensions, cropDimensions) {
     let clipInfo = {
         clipRight:  pngImage.width,
         clipBottom: pngImage.height,
@@ -110,30 +107,22 @@ export function calculateClipInfo (pngImage, path, markSeed, clientAreaDimension
         clipTop:    0,
     };
 
-    let markPosition = null;
-
-    if (markSeed && clientAreaDimensions) {
-        markPosition = calculateMarkPosition(pngImage, markSeed);
-
-        if (!markPosition)
-            throw new Error(renderTemplate(WARNING_MESSAGES.screenshotMarkNotFound, path, markSeedToId(markSeed)));
-
-        clipInfo = getClipInfoByMarkPosition(markPosition, clientAreaDimensions);
-    }
+    if (markSeedPosition && clientAreaDimensions)
+        clipInfo = getClipInfoByMarkPosition(markSeedPosition, clientAreaDimensions);
 
     clipInfo = getClipInfoByCropDimensions(clipInfo, cropDimensions);
 
-    if (markPosition && markPosition.y === clipInfo.clipBottom)
+    if (markSeedPosition && markSeedPosition.y === clipInfo.clipBottom)
         clipInfo.clipBottom--;
 
     return clipInfo;
 }
 
-export async function cropScreenshot (image, { path, markSeed, clientAreaDimensions, cropDimensions }) {
-    if (!markSeed && !cropDimensions)
+export async function cropScreenshot (image, { path, markSeedPosition, clientAreaDimensions, cropDimensions }) {
+    if (!markSeedPosition && !cropDimensions)
         return null;
 
-    const clip = calculateClipInfo(image, path, markSeed, clientAreaDimensions, cropDimensions);
+    const clip = calculateClipInfo(image, markSeedPosition, clientAreaDimensions, cropDimensions);
 
     await validateClipInfo(clip, path);
 
diff --git a/src/utils/raw-command-callsite-record.ts b/src/utils/raw-command-callsite-record.ts
index 823178224..69bb4c296 100644
--- a/src/utils/raw-command-callsite-record.ts
+++ b/src/utils/raw-command-callsite-record.ts
@@ -84,10 +84,10 @@ export const renderers = {
 };
 
 export class RawCommandCallsiteRecord {
-    public readonly actionId: number;
+    public readonly actionId: string;
     private readonly _list: Command[];
 
-    public constructor (actionId: number, list: Command[]) {
+    public constructor (actionId: string, list: Command[]) {
         this.actionId = actionId;
         this._list    = list;
     }


Test patch:
diff --git a/src/compiler/test-file/formats/raw.js b/src/compiler/test-file/formats/raw.js
index 59db4f4a6..9fc04bb6a 100644
--- a/src/compiler/test-file/formats/raw.js
+++ b/src/compiler/test-file/formats/raw.js
@@ -13,12 +13,12 @@ export default class RawTestFileCompiler extends TestFileCompilerBase {
         return async t => {
             for (let i = 0; i < commands.length; i++) {
                 const {
-                    actionId: initActionId,
                     callsite: initCallsite,
+                    actionId,
                     ...commandObj
                 } = commands[i];
-                const actionId = initActionId || parseInt(initCallsite, 10);
-                const callsite = actionId ? new RawCommandCallsiteRecord(actionId, commands) : initCallsite || initActionId;
+
+                const callsite = actionId ? new RawCommandCallsiteRecord(actionId, commands) : initCallsite || actionId;
 
                 try {
                     const command = createCommandFromObject(commandObj, t.testRun);
@@ -26,8 +26,8 @@ export default class RawTestFileCompiler extends TestFileCompilerBase {
                     await t.testRun.executeCommand(command, callsite);
                 }
                 catch (err) {
-                    err.callsite  = callsite;
-                    err.actionId  = initActionId;
+                    err.callsite = callsite;
+                    err.actionId = actionId;
                     throw err;
                 }
             }
diff --git a/src/test-run/browser-manipulation-queue.js b/src/test-run/browser-manipulation-queue.js
index 9aa7196d3..9f79181e6 100644
--- a/src/test-run/browser-manipulation-queue.js
+++ b/src/test-run/browser-manipulation-queue.js
@@ -3,8 +3,6 @@ import { isServiceCommand } from './commands/utils';
 import COMMAND_TYPE from './commands/type';
 import WARNING_MESSAGE from '../notifications/warning-message';
 import { WindowDimensionsOverflowError } from '../errors/test-run/';
-import { TEST_RUN_ERRORS } from '../errors/types';
-
 
 export default class BrowserManipulationQueue {
     constructor (browserConnection, screenshotCapturer, warningLog) {
@@ -51,16 +49,7 @@ export default class BrowserManipulationQueue {
     }
 
     async _takeScreenshot (capture) {
-        try {
-            return await capture();
-        }
-        catch (err) {
-            if (err.code === TEST_RUN_ERRORS.invalidElementScreenshotDimensionsError)
-                throw err;
-
-            this.warningLog.addWarning(WARNING_MESSAGE.screenshotError, err.stack);
-            return null;
-        }
+        return capture();
     }
 
     async _executeCommand (driverMsg) {
diff --git a/test/functional/fixtures/api/raw/test-controller/test.js b/test/functional/fixtures/api/raw/test-controller/test.js
index 1ab93c299..fa533b8fe 100644
--- a/test/functional/fixtures/api/raw/test-controller/test.js
+++ b/test/functional/fixtures/api/raw/test-controller/test.js
@@ -3,7 +3,7 @@ const { expect } = require('chai');
 // NOTE: we run tests in chrome only, because we mainly test server API functionality.
 // Actions functionality is tested in lower-level raw API.
 describe('[Raw API] TestController', () => {
-    it('Should produce correct callsites for chained calls in raw test', () => {
+    it('Should produce correct callsites for chained calls', () => {
         return runTests('./testcafe-fixtures/test-controller-test.testcafe', 'Chaining callsites', {
             shouldFail: true,
             only:       'chrome',
@@ -17,7 +17,21 @@ describe('[Raw API] TestController', () => {
             });
     });
 
-    it('Should produce correct callsites with assertions for chained calls in raw test', () => {
+    it('Should produce correct callsites for chained calls in hooks', () => {
+        return runTests('./testcafe-fixtures/hooks-test.testcafe', 'Chaining callsites in hooks', {
+            shouldFail: true,
+            only:       'chrome',
+        })
+            .catch(errs => {
+                expect(errs[0]).to.contains(
+                    ' 2 |Click (Selector(\'#btn2\'))' +
+                    ' > 3 |Click (Selector(\'#errorrr\'))' +
+                    ' 4 |Click (Selector(\'#btn3\'))'
+                );
+            });
+    });
+
+    it('Should produce correct callsites with assertions for chained calls', () => {
         return runTests('./testcafe-fixtures/test-controller-test.testcafe', 'Chaining callsites with assertions', {
             shouldFail: true,
             only:       'chrome',
diff --git a/test/functional/fixtures/api/raw/test-controller/testcafe-fixtures/hooks-test.testcafe b/test/functional/fixtures/api/raw/test-controller/testcafe-fixtures/hooks-test.testcafe
new file mode 100644
index 000000000..602e1539a
--- /dev/null
+++ b/test/functional/fixtures/api/raw/test-controller/testcafe-fixtures/hooks-test.testcafe
@@ -0,0 +1,44 @@
+{
+    "fixtures":[
+        {
+            "name":"[API] TestController",
+            "pageUrl":"http://localhost:3000/fixtures/api/es-next/test-controller/pages/index.html",
+            "tests":[
+                {
+                    "name":"Chaining callsites in hooks",
+                    "commands":[]
+                }
+            ],
+            "beforeEachCommands":[
+                {
+                    "selector": {"type":"js-expr","value":"Selector('#btn1')"},
+                    "studio": {"selectors":[{"rawSelector":{"type":"js-expr","value":"Selector('#btn1')"},"ruleType":"id"},{"rawSelector":{"type":"js-expr","value":"Selector('form input')"},"ruleType":"$dom$"}],"useOffsets":false,"offsetX":18,"offsetY":12},
+                    "options": {},
+                    "type": "click",
+                    "actionId": "0"
+                },
+                {
+                    "selector": {"type":"js-expr","value":"Selector('#btn2')"},
+                    "studio": {"selectors":[{"rawSelector":{"type":"js-expr","value":"Selector('#btn2')"},"ruleType":"id"},{"rawSelector":{"type":"js-expr","value":"Selector('form input').nth(1)"},"ruleType":"$dom$"}],"useOffsets":false,"offsetX":30,"offsetY":12},
+                    "options": {},
+                    "type": "click",
+                    "actionId": "1"
+                },
+                {
+                    "selector": {"type":"js-expr","value":"Selector('#errorrr')"},
+                    "studio": {"selectors":[{"rawSelector":{"type":"js-expr","value":"Selector('#error')"},"ruleType":"id"},{"rawSelector":{"type":"js-expr","value":"Selector('form input').nth(3)"},"ruleType":"$dom$"}],"useOffsets":false,"offsetX":14,"offsetY":10},
+                    "options":{},
+                    "type":"click",
+                    "actionId":"2"
+                },
+                {
+                    "selector":{"type":"js-expr","value":"Selector('#btn3')"},
+                    "studio":{"selectors":[{"rawSelector":{"type":"js-expr","value":"Selector('#btn3')"},"ruleType":"id"},{"rawSelector":{"type":"js-expr","value":"Selector('form input').nth(2)"},"ruleType":"$dom$"}],"useOffsets":false,"offsetX":28,"offsetY":8},
+                    "options":{},
+                    "type":"click",
+                    "actionId":"3"
+                }
+            ]
+        }
+    ]
+}
\ No newline at end of file
diff --git a/test/server/capturer-test.js b/test/server/capturer-test.js
index 7f27aaadf..f68c580e5 100644
--- a/test/server/capturer-test.js
+++ b/test/server/capturer-test.js
@@ -6,6 +6,12 @@ const { statSync }               = require('fs');
 const Capturer                   = require('../../lib/screenshots/capturer');
 const TestRunController          = require('../../lib/runner/test-run-controller');
 const Screenshots                = require('../../lib/screenshots');
+const {
+    writePng,
+    deleteFile,
+    readPng,
+}               = require('../../lib/utils/promisified-functions');
+const WarningLog                 = require('../../lib/notifications/warning-log');
 
 
 const filePath = resolve(process.cwd(), `temp${nanoid(7)}`, 'temp.png');
@@ -111,4 +117,61 @@ describe('Capturer', () => {
             takenOnFail:       false,
         });
     });
+
+    it('Should not delete screenshot if unable to locate the page area', async () => {
+        const warningLog = new WarningLog();
+        const customPath = `${nanoid(7)}screenshot.png`;
+
+        const screenshots = new ScreenshotsMock({
+            enabled:     true,
+            path:        process.cwd(),
+            pathPattern: '',
+            fullPage:    false,
+        });
+
+        const providerMock = {
+            takeScreenshot: async (_, path) => {
+                const image = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAEBgIApD5fRAAAAABJRU5ErkJggg==', 'base64');
+                const png   = await readPng(image);
+
+                await writePng(path, png);
+            },
+        };
+
+        const testRunControllerMock = {
+            _screenshots: screenshots,
+            test:         { fixture: {} },
+            emit:         noop,
+            _warningLog:  warningLog,
+            _testRunCtor: function ({ browserConnection }) {
+                this.id                = 'test-run-id';
+                this.browserConnection = browserConnection;
+                this.initialize        = noop;
+            },
+        };
+
+        await screenshots._onMessageBusStart();
+
+        await TestRunController.prototype._createTestRun.call(testRunControllerMock, {
+            id:          'browser-connection-id',
+            provider:    providerMock,
+            browserInfo: {
+                parsedUserAgent: {
+                    os: {
+                        name: 'os-name',
+                    },
+                },
+            },
+        });
+
+        await screenshots.capturer._capture(false, {
+            actionId: 'action-id',
+            markSeed: Buffer.from([255, 255, 255, 255]),
+            customPath,
+        });
+
+        expect(warningLog.messages[0]).contain('Unable to locate the page area in the browser window screenshot at');
+
+        await deleteFile(customPath);
+    });
 });
diff --git a/test/server/crop-test.js b/test/server/crop-test.js
index 866ac7b61..10d805fe1 100644
--- a/test/server/crop-test.js
+++ b/test/server/crop-test.js
@@ -1,14 +1,6 @@
-const { nanoid }  = require('nanoid');
 const expect      = require('chai').expect;
-const { resolve } = require('path');
-
-const { writePng, readPng, deleteFile, readPngFile } = require('../../lib/utils/promisified-functions');
-
-const image          = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M/wHwAEBgIApD5fRAAAAABJRU5ErkJggg==', 'base64');
-const screenshotPath = resolve(process.cwd(), `temp${nanoid(7)}.png`);
 
 const {
-    cropScreenshot,
     getClipInfoByCropDimensions,
     calculateMarkPosition,
     getClipInfoByMarkPosition,
@@ -138,61 +130,21 @@ describe('Crop images', () => {
             bottom: 850,
         };
 
-        expect(calculateClipInfo(getPngMock(), 'path', markSeed, clientAreaDimensions)).eql({
+        const pngImage     = getPngMock();
+        const markPosition = calculateMarkPosition(pngImage, markSeed);
+
+        expect(calculateClipInfo(pngImage, markPosition, clientAreaDimensions)).eql({
             clipLeft:   0,
             clipTop:    0,
             clipRight:  1820,
             clipBottom: 953,
         });
 
-        expect(calculateClipInfo(getPngMock(), 'path', markSeed, clientAreaDimensions, cropDimensions)).eql({
+        expect(calculateClipInfo(pngImage, markPosition, clientAreaDimensions, cropDimensions)).eql({
             clipLeft:   20,
             clipTop:    20,
             clipRight:  1800,
             clipBottom: 850,
         });
     });
-
-    it('Throw error if mark is not found', () => {
-        let err = null;
-
-        try {
-            calculateClipInfo(getPngMock(), 'path', '+', { width: 1620, height: 854 });
-        }
-        catch (e) {
-            err = e;
-        }
-        finally {
-            expect(err.message).is.not.null;
-            expect(err.message).contains(
-                'Unable to locate the page area in the browser window screenshot at path, ' +
-                'because the page area mark with ID 2147483648 ' +
-                'is not found in the screenshot.');
-        }
-    });
-
-    it('Should not delete screenshot if unable to locate the page area', async () => {
-        let err   = null;
-        const png = await readPng(image);
-
-        await writePng(screenshotPath, png);
-
-        const clientAreaDimensions = { width: 1620, height: 854 };
-
-        try {
-            await cropScreenshot(png, { path: screenshotPath, markSeed: '+', clientAreaDimensions });
-        }
-        catch (e) {
-            err = e;
-        }
-        finally {
-            expect(err.message).contains('Unable to locate the page area in the browser window screenshot');
-
-            const file = await readPngFile(screenshotPath);
-
-            expect(file).is.not.null;
-
-            await deleteFile(screenshotPath);
-        }
-    });
 });