import BaseDialog from './BaseDialog'; const VB_DIALOG_CONTAINER = '#virtual-background-dialog'; const NONE_THUMBNAIL = '[aria-label="Remove background"]'; const SLIGHT_BLUR_THUMBNAIL = '[aria-label="Half Blur"]'; const BLUR_THUMBNAIL = '[aria-label="Blur"]'; /** * Page object for the Virtual Background tab inside the settings dialog. */ export default class VirtualBackgroundDialog extends BaseDialog { /** * Waits for the virtual background dialog container to be displayed. */ async waitForDisplay(): Promise { await this.participant.driver.$(VB_DIALOG_CONTAINER).waitForDisplayed({ timeout: 5000, timeoutMsg: 'Virtual background dialog not displayed' }); } /** * Returns true if the dialog container is currently displayed. */ isDisplayed(): Promise { return this.participant.driver.$(VB_DIALOG_CONTAINER).isDisplayed(); } /** * Clicks a thumbnail and waits for it to become aria-checked. * Ensures React has finished re-rendering before the next click, preventing * rapid clicks from being swallowed during reconciliation. * * @param {string} selector - CSS selector for the thumbnail. * @param {string} label - Human-readable label for logging. */ private async clickAndWaitChecked(selector: string, label: string): Promise { await this.participant.log(`VirtualBackgroundDialog: clicking ${label} thumbnail`); const el = this.participant.driver.$(selector); await el.waitForClickable({ timeout: 3000, timeoutMsg: `${label} thumbnail not clickable` }); // Selecting an effect makes the preview (re)load the segmentation model/wasm, which // saturates the main thread. Two flaky failure modes follow from that: // 1. A click issued while React is mid-reconciliation (e.g. during rapid switching) is // dropped — the onClick handler never fires and the thumbnail never becomes checked. // 2. The first selection of a blur effect triggers a cold model/worker init that blocks // the main thread for several seconds, delaying the aria-checked re-render (and the // attribute reads themselves) well past a short timeout. // Re-issue the click on every poll until aria-checked confirms it landed (covers 1), and // allow a generous timeout for the cold-start re-render under load (covers 2). The poll // interval is kept large so a still-pending selection is not needlessly re-triggered. await this.participant.driver.waitUntil( async () => { if (await el.getAttribute('aria-checked') === 'true') { return true; } await el.click(); return false; }, { timeout: 15000, interval: 1500, timeoutMsg: `${label} thumbnail did not become checked after click` } ); } /** * Clicks the "Remove background" (None) thumbnail. */ clickNone(): Promise { return this.clickAndWaitChecked(NONE_THUMBNAIL, 'None'); } /** * Clicks the "Half Blur" (slight blur) thumbnail. */ clickSlightBlur(): Promise { return this.clickAndWaitChecked(SLIGHT_BLUR_THUMBNAIL, 'Slight Blur'); } /** * Clicks the "Blur" (full blur) thumbnail. */ clickBlur(): Promise { return this.clickAndWaitChecked(BLUR_THUMBNAIL, 'Blur'); } /** * Returns true if the given thumbnail element has aria-checked="true" (selected in the dialog). * * @param {string} selector - CSS selector for the thumbnail element. */ async isThumbnailChecked(selector: string): Promise { const el = this.participant.driver.$(selector); await el.waitForExist({ timeout: 2000 }); return (await el.getAttribute('aria-checked')) === 'true'; } /** * Returns true if the None thumbnail is currently selected in the dialog. */ isNoneChecked(): Promise { return this.isThumbnailChecked(NONE_THUMBNAIL); } /** * Returns true if the Slight Blur thumbnail is currently selected in the dialog. */ isSlightBlurChecked(): Promise { return this.isThumbnailChecked(SLIGHT_BLUR_THUMBNAIL); } /** * Returns true if the Blur thumbnail is currently selected in the dialog. */ isBlurChecked(): Promise { return this.isThumbnailChecked(BLUR_THUMBNAIL); } /** * Clicks OK to apply the selected background and close the dialog. */ confirm(): Promise { return this.clickOkButton(); } /** * Clicks the X button to cancel without applying any changes. */ cancel(): Promise { return this.clickCloseButton(); } }