diff --git a/playwright.config.ts b/playwright.config.ts index 3787807dd..625483e63 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -80,7 +80,7 @@ const config = defineConfig({ actionTimeout: 15 * 1000, /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'https://app.qa.cboard.io', + baseURL: process.env.PLAYWRIGHT_BASE_URL || 'https://app.qa.cboard.io', /* Longer navigation timeout for slow environments */ navigationTimeout: 60 * 1000, diff --git a/src/components/Settings/Settings.component.js b/src/components/Settings/Settings.component.js index 83b36a0d2..c8fab999f 100644 --- a/src/components/Settings/Settings.component.js +++ b/src/components/Settings/Settings.component.js @@ -46,6 +46,13 @@ const propTypes = { }; export class Settings extends PureComponent { + state = { isDialogEntered: false }; + + handleDialogEntered = () => { + // Tour targets must be measured after the dialog's slide transition settles. + this.setState({ isDialogEntered: true }); + }; + getSettingsSections() { const { isLogged, logout, user, isInFreeCountry } = this.props; @@ -242,6 +249,7 @@ export class Settings extends PureComponent { open title={} onClose={this.handleGoBack} + TransitionProps={{ onEntered: this.handleDialogEntered }} buttons={ isSettingsLocation && !isSettingsTourEnabled && @@ -274,13 +282,15 @@ export class Settings extends PureComponent { key={index} /> ))} - {isSettingsLocation && isSettingsTourEnabled && ( - - )} + {isSettingsLocation && + isSettingsTourEnabled && + this.state.isDialogEntered && ( + + )} ); } diff --git a/src/components/Settings/Settings.css b/src/components/Settings/Settings.css index abcf1e014..2dc23fa6a 100644 --- a/src/components/Settings/Settings.css +++ b/src/components/Settings/Settings.css @@ -17,6 +17,11 @@ margin-bottom: 8px; } +/* Joyride measures row offsets from the fixed dialog, not individual lists. */ +.Settings__section .Settings__list { + position: static; +} + .adSense__marker { display: none; height: 198px; diff --git a/src/components/Settings/SettingsSection.component.js b/src/components/Settings/SettingsSection.component.js index 19353d69e..21fd5a4f9 100644 --- a/src/components/Settings/SettingsSection.component.js +++ b/src/components/Settings/SettingsSection.component.js @@ -30,6 +30,7 @@ export class SettingsSection extends PureComponent { return ( @@ -55,11 +56,8 @@ export class SettingsSection extends PureComponent { return ( - -
+ +
{item.icon} } diff --git a/src/components/Settings/SettingsTour.component.js b/src/components/Settings/SettingsTour.component.js index 938937816..dcef9e585 100644 --- a/src/components/Settings/SettingsTour.component.js +++ b/src/components/Settings/SettingsTour.component.js @@ -315,11 +315,11 @@ function SettingsTour({ intl, disableTour, isSettingsTourEnabled }) { steps={settingsTourSteps} continuous={true} showSkipButton={true} - //disableScrollParentFix={true} + disableScrollParentFix={true} showProgress={false} disableOverlayClose={true} run={isSettingsTourEnabled} - scrollOffset={500} + scrollOffset={20} spotlightPadding={4} styles={joyRideStyles} scrollDuration={100} diff --git a/src/components/UI/FullScreenDialog/FullScreenDialog.js b/src/components/UI/FullScreenDialog/FullScreenDialog.js index ff4052de8..aa5f2574e 100644 --- a/src/components/UI/FullScreenDialog/FullScreenDialog.js +++ b/src/components/UI/FullScreenDialog/FullScreenDialog.js @@ -23,7 +23,8 @@ FullScreenDialog.propTypes = { onSubmit: PropTypes.func, classes: PropTypes.object.isRequired, className: PropTypes.string, - children: PropTypes.node + children: PropTypes.node, + TransitionProps: PropTypes.object }; FullScreenDialog.defaultProps = { @@ -96,7 +97,8 @@ function FullScreenDialog(props) { onClose, onSubmit, transition = transitions.UP, - fullWidth + fullWidth, + TransitionProps } = props; const theme = useTheme(); @@ -107,6 +109,7 @@ function FullScreenDialog(props) { fullScreen open={open} TransitionComponent={getTransition(transition)} + TransitionProps={TransitionProps} onClose={onClose} > diff --git a/tests/unlogged/settings/tour.spec.js b/tests/unlogged/settings/tour.spec.js new file mode 100644 index 000000000..c5762b303 --- /dev/null +++ b/tests/unlogged/settings/tour.spec.js @@ -0,0 +1,125 @@ +import { test, expect } from '@playwright/test'; + +const nextButton = '[data-test-id="button-primary"]'; +const tourSteps = [ + 'Language', + 'Speech', + 'Export', + 'Import', + 'Display', + 'Scanning', + 'Navigation' +]; + +for (const lang of ['en-US', 'es-ES', 'ar-SA']) { + for (const dark of [false, true]) { + test(`Settings tour: ${lang}, ${dark ? 'dark' : 'light'} theme`, async ({ + page + }, testInfo) => { + const joyrideErrors = []; + page.on('console', (message) => { + if ( + ['warning', 'error'].includes(message.type()) && + /joyride|target not mounted/i.test(message.text()) + ) { + joyrideErrors.push(message.text()); + } + }); + await page.addInitScript( + ({ lang }) => { + localStorage.setItem( + 'persist:language', + JSON.stringify({ + lang: JSON.stringify(lang), + dir: JSON.stringify(lang === 'ar-SA' ? 'rtl' : 'ltr') + }) + ); + }, + { lang } + ); + await page.goto('/settings'); + await expect(page.locator('[role="alertdialog"]')).toBeVisible(); + await expect(page.locator('html')).toHaveAttribute('lang', lang); + await expect(page.locator('html')).toHaveAttribute( + 'dir', + lang === 'ar-SA' ? 'rtl' : 'ltr' + ); + + if (dark) { + await page.locator('[data-test-id="button-skip"]').click(); + await page.locator('#Display').click(); + await page.locator('.Display__Options input[type="checkbox"]').check(); + await page.locator('#save-button').click(); + await expect(page.locator('.App')).toHaveClass(/is-dark/); + await page.locator('.Settings_EnableTour_Button button').click(); + } + + for (const setting of tourSteps) { + await page.locator(nextButton).click(); + const target = page.locator(`#${setting}`); + const spotlight = page.locator('.react-joyride__spotlight'); + await expect(spotlight).toBeVisible(); + await expect + .poll( + async () => { + const row = await target.boundingBox(); + const highlight = await spotlight.boundingBox(); + if (!row || !highlight) return Infinity; + return Math.max( + Math.abs(highlight.x - (row.x - 4)), + Math.abs(highlight.y - (row.y - 4)), + Math.abs(highlight.width - (row.width + 8)), + Math.abs(highlight.height - (row.height + 8)) + ); + }, + { message: `${setting} spotlight should surround its setting` } + ) + .toBeLessThanOrEqual(2); + await expect(target).toBeInViewport({ ratio: 1 }); + await expect(page.locator('[role="alertdialog"]')).toBeInViewport({ + ratio: 1 + }); + if (['Display', 'Scanning', 'Navigation'].includes(setting)) { + await expect + .poll( + () => + page + .locator('[role="alertdialog"] img') + .evaluateAll( + (images) => + images.length > 0 && + images.every( + (image) => image.complete && image.naturalWidth > 0 + ) + ), + { message: `${setting} tour images should load` } + ) + .toBe(true); + } + if (setting === 'Export') { + await testInfo.attach('export-step', { + body: await page.screenshot(), + contentType: 'image/png' + }); + } + } + + await page.locator(nextButton).click(); + await expect(page.locator('[role="alertdialog"]')).toBeHidden(); + await page.reload(); + await expect(page.locator('#Language')).toBeVisible(); + await expect(page.locator('[role="alertdialog"]')).toBeHidden(); + await page.locator('.Settings_EnableTour_Button button').click(); + await expect(page.locator('[role="alertdialog"]')).toBeVisible(); + await page.locator('[data-test-id="button-skip"]').click(); + await page.reload(); + await expect( + page.locator('.Settings_EnableTour_Button button') + ).toBeVisible(); + await expect(page.locator('[role="alertdialog"]')).toBeHidden(); + await page.locator('.Settings_EnableTour_Button button').click(); + await expect(page.locator('[role="alertdialog"]')).toBeVisible(); + expect(joyrideErrors).toEqual([]); + }); + } +}