Skip to content

Commit

Permalink
feat(ui5-barcode-scanner-dialog): added capture region overlay (#9646)
Browse files Browse the repository at this point in the history
This feature introduces the ability to scan a specific region of the screen within the BarcodeScannerDialog component. By focusing on a designated capture area, it improves scanning accuracy and ensures only the barcode in that region is processed.

A square area in the center of the video feed, highlighted by a red border, shows the active scanning zone. The rest of the screen is darkened with a semi-transparent overlay.
  • Loading branch information
kgogov authored Oct 15, 2024
1 parent 273c015 commit 19475eb
Show file tree
Hide file tree
Showing 5 changed files with 668 additions and 141 deletions.
349 changes: 349 additions & 0 deletions packages/fiori/cypress/specs/BarcodeScannerDialog.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
import { html } from "lit";
import "@ui5/webcomponents-icons/dist/camera.js";
import "../../src/BarcodeScannerDialog.js";
import type BarcodeScannerDialog from "../../src/BarcodeScannerDialog.js";

describe("BarcodeScannerDialog", () => {
let handleScanSuccess: (event: CustomEvent) => void;
let handleScanError: (event: CustomEvent) => void;

beforeEach(() => {
cy.mount(html`
<ui5-barcode-scanner-dialog id="dlgScan"></ui5-barcode-scanner-dialog>
<ui5-button id="btnScan" icon="camera">Open Scanner Dialog</ui5-button>
<div>
<ui5-label id="scanResult"></ui5-label>
<ui5-label id="scanError"></ui5-label>
</div>
`);

cy.get("#dlgScan").as("dialog");
cy.get("#btnScan").as("button");
cy.get("@dialog")
.shadow()
.find(".ui5-barcode-scanner-dialog-video")
.as("videoElement");

// Add event listener to the button to open the dialog
cy.document().then(doc => {
const dlgScan = doc.querySelector<BarcodeScannerDialog>("#dlgScan")!;
const btnScan = doc.querySelector("#btnScan")!;

btnScan.addEventListener("click", () => {
dlgScan.open = true;
});
});
});

afterEach(() => {
// Remove event listeners if they were added
cy.document().then(doc => {
const dlgScan = doc.querySelector<BarcodeScannerDialog>("#dlgScan")!;
if (handleScanSuccess) {
dlgScan.removeEventListener("scan-success", handleScanSuccess as EventListener);
handleScanSuccess = undefined!;
}
if (handleScanError) {
dlgScan.removeEventListener("scan-error", handleScanError as EventListener);
handleScanError = undefined!;
}

// Clear the scan result and error
const scanResultElement = doc.querySelector("#scanResult")!;
const scanErrorElement = doc.querySelector("#scanError")!;
scanResultElement.textContent = "";
scanErrorElement.textContent = "";
});
});

it("should open and close the dialog", () => {
// Before clicking, the dialog should not be open
cy.get("@dialog")
.shadow()
.find("ui5-dialog")
.should("not.have.attr", "open");

// Click the button to open the dialog
cy.get("@button").realClick();

// Wait for the video to be ready
cy.get("@videoElement").should($video => {
const videoEl = $video[0] as HTMLVideoElement;
expect(videoEl.readyState, "Video readyState should be >= 1").to.be.at.least(1);
});

// Assert that the dialog is open
cy.get("@dialog")
.shadow()
.find("ui5-dialog")
.should("have.attr", "open");

// Close the dialog using the close button
cy.get("@dialog")
.shadow()
.find("ui5-dialog [slot=footer] ui5-button")
.realClick();

// Verify the dialog is closed
cy.get("@dialog")
.shadow()
.find("ui5-dialog")
.should("not.have.attr", "open");
});

it("closes barcode scanner dialog with escape key", () => {
// Click the button to open the dialog
cy.get("@button").realClick();

// Wait for the video to be ready
cy.get("@videoElement").should($video => {
const videoEl = $video[0] as HTMLVideoElement;
expect(videoEl.readyState, "Video readyState should be >= 1").to.be.at.least(1);
});

// Assert that the dialog is open
cy.get("@dialog")
.shadow()
.find("ui5-dialog")
.should("have.attr", "open");

// Simulate pressing the Escape key
cy.get("@dialog").realPress("Escape");

// Verify the dialog is closed
cy.get("@dialog")
.shadow()
.find("ui5-dialog")
.should("not.have.attr", "open");
});

it("opens barcode scanner dialog and checks video stream", () => {
// Click the button to open the dialog
cy.get("@button").realClick();

// Wait for the video to be ready
cy.get("@videoElement").should($video => {
const videoEl = $video[0] as HTMLVideoElement;
expect(videoEl.readyState, "Video readyState should be >= 1").to.be.at.least(1);
});

// Assert that the dialog is open
cy.get("@dialog")
.shadow()
.find("ui5-dialog")
.should("have.attr", "open");

// Check that video.srcObject is not null
cy.get("@videoElement").should($video => {
const videoEl = $video[0] as HTMLVideoElement;
// eslint-disable-next-line no-unused-expressions
expect(videoEl.srcObject, "Video srcObject should not be null").to.not.be.null;
});

// Get the overlay canvas element
cy.get("@dialog")
.shadow()
.find(".ui5-barcode-scanner-dialog-overlay")
.as("overlayElement")
.should("be.visible");

// Check that the canvas dimensions match the video element's dimensions
cy.get("@videoElement").then($video => {
cy.get("@overlayElement").then($canvas => {
expect(
($canvas[0] as HTMLCanvasElement).width,
"Canvas width should match video width",
).to.equal($video[0].clientWidth);
expect(
($canvas[0] as HTMLCanvasElement).height,
"Canvas height should match video height",
).to.equal($video[0].clientHeight);
});
});
});

it("stops media tracks when the dialog is closed", () => {
// Click the button to open the dialog
cy.get("@button").realClick();

// Wait for the video to be ready
cy.get("@videoElement").should($video => {
const videoEl = $video[0] as HTMLVideoElement;
expect(videoEl.readyState, "Video readyState should be >= 1").to.be.at.least(1);
});

// Assert that the dialog is open
cy.get("@dialog")
.shadow()
.find("ui5-dialog")
.should("have.attr", "open");

// Save the media stream
cy.get("@videoElement").then($video => {
const videoEl = $video[0] as HTMLVideoElement;
cy.wrap(videoEl.srcObject).as("mediaStream");
});

// Close the dialog using the close button
cy.get("@dialog")
.shadow()
.find("ui5-dialog [slot=footer] ui5-button")
.realClick();

// Verify the dialog is closed
cy.get("@dialog")
.shadow()
.find("ui5-dialog")
.should("not.have.attr", "open");

// Check that media tracks are stopped
cy.get("@mediaStream").then(currentSubject => {
const mediaStream = currentSubject as unknown as MediaStream;
const tracksStopped = mediaStream
.getTracks()
.every(track => track.readyState === "ended");
// eslint-disable-next-line no-unused-expressions
expect(tracksStopped, "All media tracks should be stopped").to.be.true;
});

// Check that video srcObject is cleared
cy.get("@videoElement").then($video => {
const videoEl = $video[0] as HTMLVideoElement;
// eslint-disable-next-line no-unused-expressions
expect(videoEl.srcObject, "Video srcObject should be null").to.be.null;
});
});

it("displays the busy indicator while loading", () => {
// Stub the _getUserPermission method to simulate delay
cy.get("@dialog").then($dialog => {
const dlgScan = $dialog.get(0) as BarcodeScannerDialog;

const stub = cy
.stub(dlgScan, "_getUserPermission")
.callsFake(() => new Promise(() => { })); // Never resolves
cy.wrap(stub).as("getUserPermissionStub");
});

// Open the dialog
cy.get("@button").realClick();

// Check that the busy indicator is visible
cy.get("@dialog")
.shadow()
.find(".ui5-barcode-scanner-dialog-busy")
.should("be.visible");

// Restore the stub
cy.get("@getUserPermissionStub").then(stub => {
(stub as unknown as sinon.SinonStub).restore();
});
});

it("handles scan success event", () => {
// Define the event handler function
handleScanSuccess = (event: CustomEvent) => {
const detail = event.detail;
const scanResultElement = document.querySelector("#scanResult")!;
scanResultElement.textContent = detail.text;
};

// Get the scanResult element
cy.get("#scanResult").as("scanResult");

// Add event listener to display scan result
cy.get("@dialog").then($dialog => {
const dlgScan = $dialog.get(0) as BarcodeScannerDialog;
dlgScan.addEventListener("scan-success", handleScanSuccess as EventListener);
});

// Open the dialog
cy.get("@button").realClick();

// Simulate scan success
cy.get("@dialog").then($dialog => {
const dlgScan = $dialog.get(0) as BarcodeScannerDialog;

// Simulate the scan success
dlgScan.fireEvent("scan-success", {
text: "mocked-scan-result",
rawBytes: new Uint8Array(),
});
});

// Check that the scan result is displayed
cy.get("@scanResult").should("have.text", "mocked-scan-result");
});

it("handles scan error event", () => {
// Define the event handler function
handleScanError = (event: CustomEvent) => {
const detail = event.detail;
const scanErrorElement = document.querySelector("#scanError")!;
scanErrorElement.textContent = detail.message;
};

// Get the scanError element
cy.get("#scanError").as("scanError");

// Add event listener to display scan error
cy.get("@dialog").then($dialog => {
const dlgScan = $dialog.get(0) as BarcodeScannerDialog;
dlgScan.addEventListener("scan-error", handleScanError as EventListener);
});

// Open the dialog
cy.get("@button").realClick();

// Simulate scan error
cy.get("@dialog").then($dialog => {
const dlgScan = $dialog.get(0) as BarcodeScannerDialog;

// Simulate the scan error
dlgScan.fireEvent("scan-error", {
message: "mocked-scan-error",
});
});

// Check that the scan error is displayed
cy.get("@scanError").should("have.text", "mocked-scan-error");
});

it("handles permission denied error", () => {
// Define the event handler function
handleScanError = (event: CustomEvent) => {
const detail = event.detail;
const scanErrorElement = document.querySelector("#scanError")!;
scanErrorElement.textContent = detail.message;
};

// Get the scanError element
cy.get("#scanError").as("scanError");

// Stub getUserMedia to reject with "Permission denied"
cy.window().then(win => {
const stub = cy
.stub(win.navigator.mediaDevices, "getUserMedia")
.rejects(new DOMException("Permission denied", "NotAllowedError"));
cy.wrap(stub).as("getUserMediaStub");
});

// Add event listener to display scan error
cy.get("@dialog").then($dialog => {
const dlgScan = $dialog.get(0) as BarcodeScannerDialog;
dlgScan.addEventListener("scan-error", handleScanError as EventListener);
});

// Open the dialog
cy.get("@button").realClick();

// Check that the scan error is displayed
cy.get("@scanError").should("contain.text", "Permission denied");

// Restore the stub
cy.get("@getUserMediaStub").then(stub => {
(stub as unknown as sinon.SinonStub).restore();
});
});
});
18 changes: 5 additions & 13 deletions packages/fiori/src/BarcodeScannerDialog.hbs
Original file line number Diff line number Diff line change
@@ -1,19 +1,11 @@
<ui5-dialog stretch
class="ui5-barcode-scanner-dialog-root"
.open="{{_open}}"
@ui5-before-open={{_startReader}}
@ui5-before-close={{_fireCloseEvent}}
@ui5-close={{_resetReader}}>
<ui5-dialog stretch class="ui5-barcode-scanner-dialog-root" .open="{{_open}}" @ui5-close="{{_closeDialog}}">
<div class="ui5-barcode-scanner-dialog-video-wrapper">
<video class="ui5-barcode-scanner-dialog-video"></video>
<video autoplay playsinline muted class="ui5-barcode-scanner-dialog-video"></video>
<canvas class="ui5-barcode-scanner-dialog-overlay"></canvas>
</div>
<div slot="footer" class="ui5-barcode-scanner-dialog-footer">
<ui5-button design="Transparent" @click={{_closeDialog}}>{{_cancelButtonText}}</ui5-button>
</div>
<ui5-busy-indicator
?active={{loading}}
size="L"
text="{{_busyIndicatorText}}"
class="ui5-barcode-scanner-dialog-busy">
</ui5-busy-indicator>
</ui5-dialog>
<ui5-busy-indicator class="ui5-barcode-scanner-dialog-busy" ?active={{loading}} size="L"
text="{{_busyIndicatorText}}"></ui5-busy-indicator>
Loading

0 comments on commit 19475eb

Please sign in to comment.