Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
24 changes: 17 additions & 7 deletions src/components/Settings/Settings.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -242,6 +249,7 @@ export class Settings extends PureComponent {
open
title={<FormattedMessage {...messages.settings} />}
onClose={this.handleGoBack}
TransitionProps={{ onEntered: this.handleDialogEntered }}
buttons={
isSettingsLocation &&
!isSettingsTourEnabled &&
Expand Down Expand Up @@ -274,13 +282,15 @@ export class Settings extends PureComponent {
key={index}
/>
))}
{isSettingsLocation && isSettingsTourEnabled && (
<SettingsTour
intl={intl}
disableTour={disableTour}
isSettingsTourEnabled={isSettingsTourEnabled}
/>
)}
{isSettingsLocation &&
isSettingsTourEnabled &&
this.state.isDialogEntered && (
<SettingsTour
intl={intl}
disableTour={disableTour}
isSettingsTourEnabled={isSettingsTourEnabled}
/>
)}
</FullScreenDialog>
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/components/Settings/Settings.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 3 additions & 5 deletions src/components/Settings/SettingsSection.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class SettingsSection extends PureComponent {
return (
<Paper className="Settings__section">
<List
className="Settings__list"
subheader={
<ListSubheader>
<FormattedMessage {...subheader} />
Expand All @@ -55,11 +56,8 @@ export class SettingsSection extends PureComponent {

return (
<Fragment key={index}>
<ListItem {...listItemProps}>
<div
className="Settings__Item__Container"
id={item.text.defaultMessage}
>
<ListItem {...listItemProps} id={item.text.defaultMessage}>
<div className="Settings__Item__Container">
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText
primary={<FormattedMessage {...item.text} />}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Settings/SettingsTour.component.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
7 changes: 5 additions & 2 deletions src/components/UI/FullScreenDialog/FullScreenDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -96,7 +97,8 @@ function FullScreenDialog(props) {
onClose,
onSubmit,
transition = transitions.UP,
fullWidth
fullWidth,
TransitionProps
} = props;

const theme = useTheme();
Expand All @@ -107,6 +109,7 @@ function FullScreenDialog(props) {
fullScreen
open={open}
TransitionComponent={getTransition(transition)}
TransitionProps={TransitionProps}
onClose={onClose}
>
<AppBar className={classes.appBar}>
Expand Down
125 changes: 125 additions & 0 deletions tests/unlogged/settings/tour.spec.js
Original file line number Diff line number Diff line change
@@ -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([]);
});
}
}