Add tests for the NewPageStepper

closes RCX-2171
flag=none

test plan: passes jenkins

Change-Id: I8b8027e733c946f87105256c123ba2a9dc593de7
Reviewed-on: https://gerrit.instructure.com/c/canvas-lms/+/354265
Tested-by: Service Cloud Jenkins <svc.cloudjenkins@instructure.com>
Reviewed-by: Eric Saupe <eric.saupe@instructure.com>
QA-Review: Eric Saupe <eric.saupe@instructure.com>
Product-Review: Ed Schiebel <eschiebel@instructure.com>
This commit is contained in:
Ed Schiebel 2024-08-02 16:12:42 -06:00
parent 8eeee029a4
commit b1d0a5a5b5
8 changed files with 305 additions and 47 deletions

View File

@ -22,6 +22,6 @@
}
},
"include": ["ui/*", "ui/**/*"],
"exclude": ["**/node_modules", "**/.*/", "vendor"],
"exclude": ["**/node_modules", "**/.*/", "vendor", "ui/features/gradebook/**/*", "ui/features/discussion_topics_post/**/*"],
"types": []
}

View File

@ -24,6 +24,10 @@ import {RadioInput} from '@instructure/ui-radio-input'
import {Text} from '@instructure/ui-text'
import {View} from '@instructure/ui-view'
import {useScope as useI18nScope} from '@canvas/i18n'
const I18n = useI18nScope('block-editor')
const PALETTES0 = [
['#083B64', '#0E5F8C', '#066B65', '#FFFFFF'],
['#1A2729', '#0ACC94', '#E5E9F3', '#FFFBF7'],
@ -70,13 +74,19 @@ const ColorPalette = ({paletteId, onSelectPalette}: ColorPaletteProps) => {
)
}
return (
<Flex as="div" direction="column" alignItems="center" gap="small">
<Flex
as="div"
direction="column"
alignItems="center"
gap="small"
data-testid="stepper-color-palette"
>
<Heading level="h3" id="palette-label">
Select a Color Palette
</Heading>
<View as="div" maxWidth="400px" textAlign="center">
<Text as="p">
Theme your page with a preselected, accessibility compliant color palette.
{I18n.t('Theme your page with a preselected, accessibility compliant color palette.')}
</Text>
</View>
<Flex direction="row" justifyItems="space-between" alignItems="center">

View File

@ -26,9 +26,14 @@ import React, {useCallback} from 'react'
import {Flex} from '@instructure/ui-flex'
import {Heading} from '@instructure/ui-heading'
import {RadioInput} from '@instructure/ui-radio-input'
import {ScreenReaderContent} from '@instructure/ui-a11y-content'
import {Text} from '@instructure/ui-text'
import {View} from '@instructure/ui-view'
import {useScope as useI18nScope} from '@canvas/i18n'
const I18n = useI18nScope('block-editor')
const FONTS: Record<string, string> = {
font0: "lato, 'Helvetica Neue', Helvetica, Arial, sans-serif",
font1: "'Balsamiq Sans', lato, 'Helvetica Neue', Helvetica, Arial, sans-serif",
@ -40,6 +45,12 @@ const FONTS: Record<string, string> = {
font8: 'verdana, geneva',
}
const FONTNAMES: Record<string, string> = Object.keys(FONTS).reduce((acc, key) => {
const name = FONTS[key].split(',')[0].replace(/["']/g, '')
acc[key] = name
return acc
}, {})
type FontPairingsProps = {
fontName: string
onSelectFont: (fontName: string) => void
@ -56,6 +67,7 @@ const FontPairings = ({fontName, onSelectFont}: FontPairingsProps) => {
const renderFont = (fontname: string) => {
return (
<div className={fontname} style={{marginBlockStart: '-0.5rem'}}>
<ScreenReaderContent>{FONTNAMES[fontname]}</ScreenReaderContent>
<Text size="x-large" themeOverride={{fontFamily: FONTS[fontname]}}>
Aa
</Text>
@ -71,14 +83,20 @@ const FontPairings = ({fontName, onSelectFont}: FontPairingsProps) => {
)
}
return (
<Flex as="div" direction="column" alignItems="center" gap="small">
<Flex
as="div"
direction="column"
alignItems="center"
gap="small"
data-testid="stepper-font-pairings"
>
<Heading level="h3" id="font-label">
Select Font Pairings
{I18n.t('Select Font Pairings')}
</Heading>
<View as="div" maxWidth="400px" textAlign="center">
<Text as="p">
Choose complimentary font pairings for your h1, h2, and paragraph text, or choose your own
later.
{I18n.t(`Choose complimentary font pairings for your h1, h2, and paragraph text, or choose your own
later.`)}
</Text>
</View>
<Flex direction="row" justifyItems="space-between" alignItems="center">

View File

@ -35,7 +35,11 @@ import {getScrollParent} from '../../../utils'
import {type PageSection} from './types'
import {getTemplate} from '../../../assets/templates'
type NewPageStepperProps = {
import {useScope as useI18nScope} from '@canvas/i18n'
const I18n = useI18nScope('block-editor')
export type NewPageStepperProps = {
open: boolean
onFinish: () => void
onCancel: () => void
@ -137,18 +141,18 @@ const NewPageStepper = ({open, onFinish, onCancel}: NewPageStepperProps) => {
case 3:
return <FontPairings fontName={fontName} onSelectFont={handleSelectFont} />
default:
throw new Error('Invalid step')
throw new Error(I18n.t('Invalid step'))
}
}
return (
<Modal open={open} label="Create a new page" onDismiss={onCancel} onClose={handleClosed}>
<Modal.Header>
<Heading>Create a new page</Heading>
<Heading>{I18n.t('Create a new page')}</Heading>
<CloseButton
data-instui-modal-close-button="true"
onClick={onCancel}
screenReaderLabel="Close"
screenReaderLabel={I18n.t('Close')}
placement="end"
offset="medium"
/>
@ -161,7 +165,7 @@ const NewPageStepper = ({open, onFinish, onCancel}: NewPageStepperProps) => {
margin="0 0 small"
onClick={handlePrevStep}
>
Back
{I18n.t('Back')}
</CondensedButton>
)}
{renderActiveStep()}
@ -169,7 +173,7 @@ const NewPageStepper = ({open, onFinish, onCancel}: NewPageStepperProps) => {
</Modal.Body>
<Modal.Footer>
<Button color="secondary" onClick={onCancel}>
Cancel
{I18n.t('Cancel')}
</Button>
<Button
color="primary"
@ -177,7 +181,11 @@ const NewPageStepper = ({open, onFinish, onCancel}: NewPageStepperProps) => {
onClick={handleNextStep}
interaction={isTemplateButtonDisabled ? 'disabled' : 'enabled'}
>
{isTemplateSelection ? 'Start Editing' : step < 3 ? 'Next' : 'Start Creating'}
{isTemplateSelection
? I18n.t('Start Editing')
: step < 3
? I18n.t('Next')
: I18n.t('Start Creating')}
</Button>
</Modal.Footer>
</Modal>

View File

@ -24,9 +24,12 @@ import {FormFieldGroup} from '@instructure/ui-form-field'
import {Heading} from '@instructure/ui-heading'
import {Text} from '@instructure/ui-text'
import {View} from '@instructure/ui-view'
import {type PageSection} from './types'
import {useScope as useI18nScope} from '@canvas/i18n'
const I18n = useI18nScope('block-editor')
type PageSectionsProps = {
selectedSections: PageSection[]
onSelectSections: (sections: PageSection[]) => void
@ -47,43 +50,54 @@ const PageSections = ({selectedSections, onSelectSections}: PageSectionsProps) =
)
return (
<Flex as="div" direction="column" alignItems="center" gap="small">
<Flex
as="div"
direction="column"
alignItems="center"
gap="small"
data-testid="stepper-page-sections"
>
<Heading level="h3">Select Page Sections</Heading>
<View as="div" maxWidth="400px" textAlign="center">
<Text as="p">
Preload your page with section placeholders. You will be able to edit, delete, or add more
sections later.
{I18n.t(`Preload your page with section placeholders. You will be able to edit, delete, or add more
sections later.`)}
</Text>
</View>
<Flex direction="row" alignItems="center" gap="medium">
<Flex.Item textAlign="start">
<FormFieldGroup layout="stacked" description="Standard">
<Checkbox
label="Hero image with text"
id="heroWithText"
label={I18n.t('Hero image with text')}
value="heroWithText"
checked={selectedSections.includes('heroWithText')}
onChange={handleChangeSelections}
/>
<Checkbox
label="Navigation"
id="navigation"
label={I18n.t('Navigation')}
value="navigation"
checked={selectedSections.includes('navigation')}
onChange={handleChangeSelections}
/>
<Checkbox
label="About (Intro)"
id="about"
label={I18n.t('About (Intro)')}
value="about"
checked={selectedSections.includes('about')}
onChange={handleChangeSelections}
/>
<Checkbox
label="Highlights or services"
id="resources"
label={I18n.t('Highlights or services')}
value="resources"
checked={selectedSections.includes('resources')}
onChange={handleChangeSelections}
/>
<Checkbox
label="Footer"
id="footer"
label={I18n.t('Footer')}
value="footer"
checked={selectedSections.includes('footer')}
onChange={handleChangeSelections}
@ -93,33 +107,38 @@ const PageSections = ({selectedSections, onSelectSections}: PageSectionsProps) =
<Flex.Item textAlign="start">
<FormFieldGroup description="Canvas" layout="stacked">
<Checkbox
label="Quiz question"
id="question"
label={I18n.t('Quiz question')}
value="question"
checked={selectedSections.includes('question')}
onChange={handleChangeSelections}
/>
<Checkbox
label="Announcement"
id="announcement"
label={I18n.t('Announcement')}
value="announcement"
checked={selectedSections.includes('announcement')}
onChange={handleChangeSelections}
/>
<Checkbox
label="Discussion topic"
id="discussion"
label={I18n.t('Discussion topic')}
value="discussion"
checked={selectedSections.includes('discussion')}
onChange={handleChangeSelections}
disabled={true}
/>
<Checkbox
label="Assignment"
id="assignment"
label={I18n.t('Assignment')}
value="assignment"
checked={selectedSections.includes('discussion')}
onChange={handleChangeSelections}
disabled={true}
/>
<Checkbox
label="Module"
id="module"
label={I18n.t('Module')}
value="module"
checked={selectedSections.includes('module')}
onChange={handleChangeSelections}

View File

@ -21,9 +21,12 @@ import {CondensedButton} from '@instructure/ui-buttons'
import {View, type ViewProps} from '@instructure/ui-view'
import {Img} from '@instructure/ui-img'
import {Text} from '@instructure/ui-text'
import {Grid} from '@instructure/ui-grid'
import {Flex} from '@instructure/ui-flex'
import {useScope as useI18nScope} from '@canvas/i18n'
const I18n = useI18nScope('block-editor')
interface PageTemplatesProps {
selectedTemplate: string
onSelectTemplate: (templateId: string) => void
@ -50,17 +53,18 @@ const PageTemplates = ({selectedTemplate = 'template-1', onSelectTemplate}: Page
)
return (
<Flex direction="column" gap="small">
<Flex direction="column" gap="small" data-testid="stepper-page-template">
<Text size="large">Browse Templates</Text>
<Text size="small">
Select from a variety of pre-designed page layouts that are ready to be filled with your
content.
{I18n.t(
'Select from a variety of pre-designed page layouts that are ready to be filled with your content.'
)}
</Text>
<Text size="large">Home Pages</Text>
<Text size="small">
A home page is the main or introductory page of a course. The home page is typically the
{I18n.t(`A home page is the main or introductory page of a course. The home page is typically the
first page you see. It serves as a central hub, guiding visitors to other parts of the
course.
course.`)}
</Text>
<View
as="div"
@ -86,7 +90,12 @@ const PageTemplates = ({selectedTemplate = 'template-1', onSelectTemplate}: Page
if (el) firstTemplateRef.current = el as HTMLButtonElement
}}
>
<Img src="/images/block_editor/template-1.png" alt="" width="230px" height="350px" />
<Img
src="/images/block_editor/template-1.png"
alt={I18n.t('template 1')}
width="230px"
height="350px"
/>
</CondensedButton>
</View>
<View
@ -103,7 +112,12 @@ const PageTemplates = ({selectedTemplate = 'template-1', onSelectTemplate}: Page
if (el) secondTemplateRef.current = el as HTMLButtonElement
}}
>
<Img src="/images/block_editor/template-2.png" alt="" width="230px" height="350px" />
<Img
src="/images/block_editor/template-2.png"
alt={I18n.t('template 2')}
width="230px"
height="350px"
/>
</CondensedButton>
</View>
</Flex>

View File

@ -25,6 +25,10 @@ import {Heading} from '@instructure/ui-heading'
import {Text} from '@instructure/ui-text'
import {View} from '@instructure/ui-view'
import {useScope as useI18nScope} from '@canvas/i18n'
const I18n = useI18nScope('block-editor')
type Step1Selection = 'scratch' | 'template'
type Step1Props = {
@ -53,7 +57,7 @@ const Step1 = ({start = 'scratch', onSelect}: Step1Props) => {
}, [onSelect])
return (
<View as="div">
<View as="div" data-testid="step-1">
<Flex direction="row" gap="large">
<Flex direction="column" width="300px">
<View
@ -68,14 +72,20 @@ const Step1 = ({start = 'scratch', onSelect}: Step1Props) => {
if (el) scratchRef.current = el as HTMLButtonElement
}}
>
<Img src="/images/block_editor/scratch.png" alt="" width="300px" height="300px" />
<Img
src="/images/block_editor/scratch.png"
alt=""
width="300px"
height="300px"
aria-labelledby="start-from-scratch-desc"
/>
</CondensedButton>
</View>
<View as="div" margin="x-small 0 0 0">
<Heading level="h3">Start from Scratch</Heading>
<View as="div" margin="x-small 0 0 0" id="start-from-scratch-desc">
<Heading level="h3">{I18n.t('Start from Scratch')}</Heading>
<Text as="p">
Select from a variety of style options or start with a blank canvas to create a page
tailored to your specific needs.
{I18n.t(`Select from a variety of style options or start with a blank canvas to create a page
tailored to your specific needs.`)}
</Text>
</View>
</Flex>
@ -92,14 +102,20 @@ const Step1 = ({start = 'scratch', onSelect}: Step1Props) => {
if (el) templateRef.current = el as HTMLButtonElement
}}
>
<Img src="/images/block_editor/template.png" alt="" width="300px" height="300px" />
<Img
src="/images/block_editor/template.png"
alt=""
width="300px"
height="300px"
aria-labelledby="select-a-template-desc"
/>
</CondensedButton>
</View>
<View as="div" margin="x-small 0 0 0">
<Heading level="h3">Select a Template</Heading>
<View as="div" margin="x-small 0 0 0" id="select-a-template-desc">
<Heading level="h3">{I18n.t('Select a Template')}</Heading>
<Text as="p">
Select from a variety of pre-designed page layouts that are ready to be filled with
your content.
{I18n.t(`Select from a variety of pre-designed page layouts that are ready to be filled with
your content.`)}
</Text>
</View>
</Flex>

View File

@ -0,0 +1,173 @@
/*
* Copyright (C) 2024 - present Instructure, Inc.
*
* This file is part of Canvas.
*
* Canvas is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, version 3 of the License.
*
* Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
* A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from 'react'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import {Editor, Frame, useEditor} from '@craftjs/core'
import {render} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {NewPageStepper} from '../NewPageStepper/NewPageStepper'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import {buildPageContent} from '../../../utils/buildPageContent'
let deserializeMock = jest.fn()
let buildPageContentMock = jest.fn()
let onFinish = jest.fn()
let onCancel = jest.fn()
jest.mock('@craftjs/core', () => {
const module = jest.requireActual('@craftjs/core')
return {
...module,
useEditor: jest.fn(() => {
return {
enabled: true,
actions: {
deserialize: () => deserializeMock(),
selectNode: jest.fn(),
},
query: {},
}
}),
}
})
jest.mock('../../../utils/buildPageContent', () => {
return {
buildPageContent: () => buildPageContentMock(),
}
})
const user = userEvent.setup()
const renderStepper = () => {
return render(
<Editor>
<NewPageStepper open={true} onFinish={onFinish} onCancel={onCancel} />
</Editor>
)
}
describe('NewPageStepper', () => {
beforeEach(() => {
deserializeMock = jest.fn()
buildPageContentMock = jest.fn()
onFinish = jest.fn()
onCancel = jest.fn()
})
it('renders', () => {
const {getByText} = renderStepper()
expect(getByText('Create a new page')).toBeInTheDocument()
expect(getByText('Start from Scratch')).toBeInTheDocument()
expect(getByText('Select a Template')).toBeInTheDocument()
expect(getByText('Close')).toBeInTheDocument()
expect(getByText('Cancel')).toBeInTheDocument()
expect(getByText('Next')).toBeInTheDocument()
})
describe('Start from Scratch', () => {
it('walks through the stepper', async () => {
const {getByText, getByTestId} = renderStepper()
const nextButton = getByText('Next').closest('button') as HTMLButtonElement
expect(nextButton).toBeInTheDocument()
const startFromScratchBtn = document
.querySelector('[aria-labelledby="start-from-scratch-desc"]')
?.closest('button') as HTMLButtonElement
expect(startFromScratchBtn).toBeInTheDocument()
await user.click(startFromScratchBtn)
await user.click(nextButton)
expect(getByTestId('stepper-page-sections')).toBeInTheDocument()
await user.click(nextButton)
expect(getByTestId('stepper-color-palette')).toBeInTheDocument()
await user.click(nextButton)
expect(getByTestId('stepper-font-pairings')).toBeInTheDocument()
const startBtn = getByText('Start Creating').closest('button') as HTMLButtonElement
expect(startBtn).toBeInTheDocument()
await user.click(startBtn)
expect(buildPageContentMock).toHaveBeenCalled()
expect(onFinish).toHaveBeenCalled()
})
})
describe('Select a Template', () => {
it('walks through the stepper', async () => {
const {getByText, getByTestId} = renderStepper()
const nextButton = getByText('Next').closest('button') as HTMLButtonElement
expect(nextButton).toBeInTheDocument()
const selectTemplateBtn = document
.querySelector('[aria-labelledby="select-a-template-desc"]')
?.closest('button') as HTMLButtonElement
expect(selectTemplateBtn).toBeInTheDocument()
await user.click(selectTemplateBtn)
await user.click(nextButton)
expect(getByTestId('stepper-page-template')).toBeInTheDocument()
await user.click(document.querySelector('#template-1') as HTMLButtonElement)
const startBtn = getByText('Start Editing').closest('button') as HTMLButtonElement
expect(startBtn).toBeInTheDocument()
await user.click(startBtn)
expect(deserializeMock).toHaveBeenCalled()
expect(onFinish).toHaveBeenCalled()
})
})
describe('misc functions', () => {
it('goes back on clicking the back button', async () => {
const {getByText, getByTestId} = renderStepper()
expect(getByTestId('step-1')).toBeInTheDocument()
const nextButton = getByText('Next').closest('button') as HTMLButtonElement
await user.click(nextButton)
expect(getByTestId('stepper-page-sections')).toBeInTheDocument()
const backBtn = getByText('Back').closest('button') as HTMLButtonElement
await user.click(backBtn)
expect(getByTestId('step-1')).toBeInTheDocument()
})
it('calls onCancel on clicking the close button', async () => {
const {getByText} = renderStepper()
const closeBtn = getByText('Close').closest('button') as HTMLButtonElement
await user.click(closeBtn)
expect(onCancel).toHaveBeenCalled()
})
it('calls onCancel on clicking hte Cancel button', async () => {
const {getByText} = renderStepper()
const cancelBtn = getByText('Cancel').closest('button') as HTMLButtonElement
await user.click(cancelBtn)
expect(onCancel).toHaveBeenCalled()
})
})
})