diff --git a/packages/fiori/cypress/specs/BarcodeScannerDialog.cy.ts b/packages/fiori/cypress/specs/BarcodeScannerDialog.cy.ts new file mode 100644 index 000000000000..e87df3fb908a --- /dev/null +++ b/packages/fiori/cypress/specs/BarcodeScannerDialog.cy.ts @@ -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` + + Open Scanner Dialog + +
+ + +
+ `); + + 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("#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("#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(); + }); + }); +}); diff --git a/packages/fiori/src/BarcodeScannerDialog.hbs b/packages/fiori/src/BarcodeScannerDialog.hbs index 00f29115596b..a1c293ee9499 100644 --- a/packages/fiori/src/BarcodeScannerDialog.hbs +++ b/packages/fiori/src/BarcodeScannerDialog.hbs @@ -1,19 +1,11 @@ - +
- + +
- -
+ \ No newline at end of file diff --git a/packages/fiori/src/BarcodeScannerDialog.ts b/packages/fiori/src/BarcodeScannerDialog.ts index cedaa80e845f..8b6f61a97363 100644 --- a/packages/fiori/src/BarcodeScannerDialog.ts +++ b/packages/fiori/src/BarcodeScannerDialog.ts @@ -8,7 +8,9 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement import property from "@ui5/webcomponents-base/dist/decorators/property.js"; import event from "@ui5/webcomponents-base/dist/decorators/event.js"; import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js"; +import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js"; import type { Result, Exception } from "@zxing/library/esm5/index.js"; +import type { Interval } from "@ui5/webcomponents-base/dist/types.js"; // eslint-disable-next-line import/no-extraneous-dependencies import ZXing from "@ui5/webcomponents-fiori/dist/ssr-zxing.js"; @@ -86,6 +88,7 @@ type BarcodeScannerDialogScanErrorEventDetail = { /** * Fired when the user closes the component. + * @since 2.0.0 * @public */ @event("close", { @@ -101,12 +104,12 @@ type BarcodeScannerDialogScanErrorEventDetail = { @event("scan-success", { detail: { /** - * @public - */ + * @public + */ text: { type: String }, /** - * @public - */ + * @public + */ rawBytes: { type: Object }, }, bubbles: true, @@ -120,8 +123,8 @@ type BarcodeScannerDialogScanErrorEventDetail = { @event("scan-error", { detail: { /** - * @public - */ + * @public + */ message: { type: String }, }, bubbles: true, @@ -134,12 +137,12 @@ class BarcodeScannerDialog extends UI5Element { * @public * @default false * @since 1.24.0 - */ + */ @property({ type: Boolean }) open = false; /** - * Indicates whether a loading indicator should be displayed in the dialog. + * Indicates whether a loading indicator should be displayed while the scanner is loading. * @default false * @private */ @@ -147,59 +150,84 @@ class BarcodeScannerDialog extends UI5Element { loading = false; /** - * Indicates whether the user has granted permissions to use the camera. + * Indicates whether the scanner is ready to scan. * @default false * @private */ @property({ type: Boolean, noAttribute: true }) - permissionsGranted = false; + isReadyToScan = false; - _codeReader: InstanceType; dialog?: Dialog; @i18n("@ui5/webcomponents-fiori") static i18nBundle: I18nBundle; + _codeReader: InstanceType; + _tempCanvas!: HTMLCanvasElement; + _scanInterval!: Interval | null; + _handleVideoPlayingBound: () => void; + _handleCaptureRegionBound: () => void; constructor() { super(); this._codeReader = new BrowserMultiFormatReader(); + this._handleVideoPlayingBound = this._handleVideoPlaying.bind(this); + this._handleCaptureRegionBound = this._handleDrawCaptureRegion.bind(this); } - onAfterRendering() { - if (this.open) { - if (this.loading) { - return; - } - - if (!this._hasGetUserMedia()) { - this.fireDecoratorEvent("scan-error", { message: "getUserMedia() is not supported by your browser" }); - return; - } - - if (!this.permissionsGranted) { - this.loading = true; - } - - this._getUserPermission() - .then(() => { - this.permissionsGranted = true; - }) - .catch(err => { - this.fireDecoratorEvent("scan-error", { message: err }); - this.loading = false; - }); - } else { + static async onDefine() { + BarcodeScannerDialog.i18nBundle = await getI18nBundle("@ui5/webcomponents-fiori"); + } + + async onAfterRendering() { + if (!this._hasGetUserMedia()) { + this.fireDecoratorEvent("scan-error", { message: "getUserMedia() is not supported by your browser" }); + return; + } + + if (!this.open || this.loading) { + return; + } + + if (!this.isReadyToScan) { + this.loading = true; + } + + const video = this._getVideoElement(); + if (video.srcObject) { + return; + } + + try { + const stream = await this._getUserPermission(); + video.addEventListener("loadeddata", this._handleVideoPlayingBound); + video.srcObject = stream; + } catch (error) { + this.fireDecoratorEvent("scan-error", { message: (error as Error).message }); this.loading = false; } } + onEnterDOM() { + super.onEnterDOM(); + window.addEventListener("resize", this._handleCaptureRegionBound); + } + + onExitDOM() { + super.onExitDOM(); + window.removeEventListener("resize", this._handleCaptureRegionBound); + } + get _open() { - return this.open && this.permissionsGranted; + return this.open && this.isReadyToScan; } - /** - * PRIVATE METHODS - */ + get _cancelButtonText() { + return BarcodeScannerDialog.i18nBundle.getText(BARCODE_SCANNER_DIALOG_CANCEL_BUTTON_TXT); + } + + get _busyIndicatorText() { + return BarcodeScannerDialog.i18nBundle.getText(BARCODE_SCANNER_DIALOG_LOADING_TXT); + } _hasGetUserMedia() { return !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia); @@ -213,48 +241,220 @@ class BarcodeScannerDialog extends UI5Element { return this.shadowRoot!.querySelector(".ui5-barcode-scanner-dialog-video")!; } - _closeDialog() { - this.open = false; + _getOverlayCanvasElement() { + return this.shadowRoot!.querySelector(".ui5-barcode-scanner-dialog-overlay")!; } - _fireCloseEvent() { - this.open = false; - this.fireDecoratorEvent("close"); - } + /** + * CALCULATIONS + * + * The following methods are used to calculate the capture region + * and draw it on the overlay canvas. + * The capture region is a square area in the center of the video element + * where the barcode scanning is performed. + * The region is defined as a proportion of the video element's dimensions. + * The overlay canvas is used to draw a semi-transparent black overlay + * over the video element and a red border around the capture region. + * The overlay canvas is updated on every frame to ensure the capture region is always visible. + * The capture region is used to crop the video frame and extract the barcode image. + * The extracted image is then processed by the zxing-js library to decode the barcode. + */ - _startReader() { - this._decodeFromCamera(); + _calculateCaptureRegion(clientWidth: number, clientHeight: number) { + // Define the maximum scan size as a proportion of the video element + const maxScanProportion = 0.66666667; // 2:3 + // Calculate maximum square dimension based on video dimensions and max proportion + const maxScanDimension = Math.min(clientWidth, clientHeight) * maxScanProportion; + // Calculate offset to center the square scan region + const xOffset = (clientWidth - maxScanDimension) / 2; + const yOffset = (clientHeight - maxScanDimension) / 2; + // Calculate the width and height of the scan region + const scanWidth = Math.floor(maxScanDimension); + const scanHeight = Math.floor(maxScanDimension); + + return { + scanHeight, + scanWidth, + xOffset, + yOffset, + }; } - _resetReader() { + _drawCaptureRegion() { const videoElement = this._getVideoElement(); - videoElement.pause(); - this._codeReader.reset(); + const canvasElement = this._getOverlayCanvasElement(); + const context = canvasElement.getContext("2d")!; + + const videoClientWidth = videoElement.clientWidth; + const videoClientHeight = videoElement.clientHeight; + + // Set canvas dimensions to match the video element's dimensions + canvasElement.width = videoClientWidth; + canvasElement.height = videoClientHeight; + + // Clear the canvas + context.clearRect(0, 0, videoClientWidth, videoClientHeight); + // Calculate the capture region + const captureRegion = this._calculateCaptureRegion(videoClientWidth, videoClientHeight); + + // Draw a semi-transparent black overlay over the video + context.fillStyle = "rgba(0, 0, 0, 0.5)"; + context.fillRect(0, 0, videoClientWidth, videoClientHeight); + context.clearRect(captureRegion.xOffset, captureRegion.yOffset, captureRegion.scanWidth, captureRegion.scanHeight); + + // Draw red border around the capture region + context.strokeStyle = "red"; + context.lineWidth = 1; + context.strokeRect(captureRegion.xOffset, captureRegion.yOffset, captureRegion.scanWidth, captureRegion.scanHeight); + + // Display the overlay + canvasElement.style.display = "block"; } - _decodeFromCamera() { - const videoElement = this._getVideoElement(); - this._codeReader.decodeFromVideoDevice(null, videoElement, (result: Result, err?: Exception) => { - this.loading = false; - if (result) { - this.fireDecoratorEvent("scan-success", - { - text: result.getText(), - rawBytes: result.getRawBytes(), - }); - } - if (err && !(err instanceof NotFoundException)) { - this.fireDecoratorEvent("scan-error", { message: err.message }); - } - }).catch((err: Error) => this.fireDecoratorEvent("scan-error", { message: err.message })); + _getTempCanvasElement() { + if (!this._tempCanvas) { + this._tempCanvas = document.createElement("canvas"); + } + return this._tempCanvas; } - get _cancelButtonText() { - return BarcodeScannerDialog.i18nBundle.getText(BARCODE_SCANNER_DIALOG_CANCEL_BUTTON_TXT); + _captureFrame() { + const video = this._getVideoElement(); + const tempCanvas = this._getTempCanvasElement(); + const context = tempCanvas.getContext("2d")!; + + const videoWidth = video.videoWidth; + const videoHeight = video.videoHeight; + const clientWidth = video.clientWidth; + const clientHeight = video.clientHeight; + + const captureRegion = this._calculateCaptureRegion(clientWidth, clientHeight); + + // Calculate the ratio of videoSize to clientSize + const ratioX = videoWidth / clientWidth; + const ratioY = videoHeight / clientHeight; + const scale = Math.min(ratioX, ratioY); + + // Calculate the scaled capture region + const scaledXOffset = captureRegion.xOffset * scale; + const scaledYOffset = captureRegion.yOffset * scale; + const scaledScanWidth = captureRegion.scanWidth * scale; + const scaledScanHeight = captureRegion.scanHeight * scale; + + // Set canvas dimensions to match the capture region dimensions + tempCanvas.width = captureRegion.scanWidth; + tempCanvas.height = captureRegion.scanHeight; + + // Clear the canvas + context.clearRect(0, 0, tempCanvas.width, tempCanvas.height); + + // Correct positioning if aspect ratios are different + const positionX = (videoWidth - clientWidth * scale) / 2; + const positionY = (videoHeight - clientHeight * scale) / 2; + + // Calculate final source position considering the video element's offset + const finalXOffset = scaledXOffset + positionX; + const finalYOffset = scaledYOffset + positionY; + + // Draw the portion of the video on the canvas + context.drawImage( + video, + finalXOffset, finalYOffset, scaledScanWidth, scaledScanHeight, // Source rectangle + 0, 0, tempCanvas.width, tempCanvas.height, // Destination rectangle + ); + + return tempCanvas; } - get _busyIndicatorText() { - return BarcodeScannerDialog.i18nBundle.getText(BARCODE_SCANNER_DIALOG_LOADING_TXT); + /** + * HANDLERS + */ + + async _processFrame() { + try { + const canvas = this._captureFrame(); + const dataUrl = canvas.toDataURL(); + const result = await this._codeReader.decodeFromImageUrl(dataUrl); + + this._handleScanSuccess(result); + } catch (error) { + this._handleScanError(error as Exception); + } + } + + _handleScanSuccess(result: Result) { + this.fireDecoratorEvent("scan-success", { + text: result.getText(), + rawBytes: result.getRawBytes(), + }); + } + + _handleScanError(error: Exception) { + if (error instanceof NotFoundException) { + return; + } + + this.fireDecoratorEvent("scan-error", { message: error.message }); + } + + _handleVideoPlaying() { + const FRAME_PROCESSING_INTERVAL = 200; // 5 frames per second + + this.loading = false; + this.isReadyToScan = true; + + // Wait for the next animation frame before drawing the capture region + requestAnimationFrame(() => { + this._drawCaptureRegion(); + }); + + // Ensure any existing interval is cleared before setting a new one + if (this._scanInterval) { + clearInterval(this._scanInterval); + } + + this._scanInterval = setInterval(() => { + this._processFrame(); + }, FRAME_PROCESSING_INTERVAL); + } + + _handleDrawCaptureRegion() { + this._drawCaptureRegion(); + } + + _closeDialog() { + this._resetReader(); + this.open = false; + this.fireDecoratorEvent("close"); + } + + _resetReader() { + const video = this._getVideoElement(); + video.pause(); + + if (video.srcObject) { + const stream = video.srcObject as MediaStream; + const tracks = stream.getTracks(); + tracks.forEach(track => track.stop()); + } + + video.srcObject = null; + video.removeEventListener("loadeddata", this._handleVideoPlayingBound); + + if (this._scanInterval) { + clearInterval(this._scanInterval); + this._scanInterval = null; + } + + if (this._codeReader) { + this._codeReader.reset(); + } + + const overlay = this._getOverlayCanvasElement(); + overlay.style.display = "none"; + + // Reset the ready state + this.isReadyToScan = false; } } diff --git a/packages/fiori/src/themes/BarcodeScannerDialog.css b/packages/fiori/src/themes/BarcodeScannerDialog.css index ed9d417658ef..a78877072a1e 100644 --- a/packages/fiori/src/themes/BarcodeScannerDialog.css +++ b/packages/fiori/src/themes/BarcodeScannerDialog.css @@ -1,34 +1,45 @@ -.ui5-barcode-scanner-dialog-root::part(content) { - padding: .4375rem; -} - -/* video */ -.ui5-barcode-scanner-dialog-video-wrapper, -.ui5-barcode-scanner-dialog-video { - height:100%; - width: 100%; -} - -.ui5-barcode-scanner-dialog-video { - object-fit: cover; -} - -/* footer */ -.ui5-barcode-scanner-dialog-footer { - display: flex; - justify-content: flex-end; - width: 100%; -} - -/* busy indicator */ -.ui5-barcode-scanner-dialog-busy { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - z-index: 1; -} - -.ui5-barcode-scanner-dialog-busy:not([active]) { - display: none; -} +.ui5-barcode-scanner-dialog-root::part(content) { + padding: .4375rem; +} + +.ui5-barcode-scanner-dialog-video-wrapper { + position: relative; +} + +/* video */ +.ui5-barcode-scanner-dialog-video-wrapper, +.ui5-barcode-scanner-dialog-video, +.ui5-barcode-scanner-dialog-overlay { + height: 100%; + width: 100%; +} + +.ui5-barcode-scanner-dialog-video { + object-fit: cover; +} + +.ui5-barcode-scanner-dialog-overlay { + display: none; + position: absolute; + inset: 0; +} + +/* footer */ +.ui5-barcode-scanner-dialog-footer { + display: flex; + justify-content: flex-end; + width: 100%; +} + +/* busy indicator */ +.ui5-barcode-scanner-dialog-busy { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 1; +} + +.ui5-barcode-scanner-dialog-busy:not([active]) { + display: none; +} \ No newline at end of file diff --git a/packages/fiori/test/specs/BarcodeScannerDialog.spec.js b/packages/fiori/test/specs/BarcodeScannerDialog.spec.js deleted file mode 100644 index bb0dc937637d..000000000000 --- a/packages/fiori/test/specs/BarcodeScannerDialog.spec.js +++ /dev/null @@ -1,25 +0,0 @@ -import { assert } from "chai"; - -describe("BarcodeScannerDialog Behavior", () => { - before(async () => { - await browser.url(`test/pages/BarcodeScannerDialog.html`); - }); - - it("fires scan-error when no permissions granted", async () => { - // Setup: deny permissions to access the camera - await browser.setPermissions({ name: 'camera' }, 'denied'); - const btnScan = await browser.$("#btnScan"), - scanError = await browser.$("#scanError"); - - await btnScan.click(); - - await browser.waitUntil(async () => { - return (await scanError.getText()).length > 0; - }, 25000, "expect scan-error output"); - - // assert - const scanErrorText = await scanError.getText(); - assert.ok(scanErrorText.length, "fires scan-error when no permissions"); - }); - -});