-
Notifications
You must be signed in to change notification settings - Fork 265
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(ui5-barcode-scanner-dialog): added capture region overlay (#9646)
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
Showing
5 changed files
with
668 additions
and
141 deletions.
There are no files selected for viewing
349 changes: 349 additions & 0 deletions
349
packages/fiori/cypress/specs/BarcodeScannerDialog.cy.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.