Skip to content

Commit 30a14ed

Browse files
JessicaSachselevatebartBarthélémy Ledoux
authored
feat: adding alert component with markdown (#19152)
Co-authored-by: ElevateBart <ledouxb@gmail.com> Co-authored-by: Barthélémy Ledoux <bart@cypress.io>
1 parent 195da20 commit 30a14ed

24 files changed

+842
-199
lines changed

.vscode/cspell.json

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"dictionaryDefinitions": [],
55
"dictionaries": [],
66
"words": [
7+
"composables",
78
"Iconify",
89
"Lachlan",
910
"msapplication",

apollo.config.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ module.exports = {
99
localSchemaFile: path.join(__dirname, 'packages/graphql/schemas/schema.graphql'),
1010
},
1111
tagName: 'gql',
12-
includes: [path.join(__dirname, 'packages/{launchpad,app,frontend-shared}/src/**/*.vue')],
12+
includes: [path.join(__dirname, 'packages/{launchpad,app,frontend-shared}/src/**/*.{vue,ts,js,tsx,jsx}')],
1313
},
1414
}

graphql-codegen.yml

+14-14
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ generates:
5050
flattenGeneratedTypes: true
5151
schema: 'packages/graphql/schemas/schema.graphql'
5252
documents:
53-
- './packages/frontend-shared/src/gql-components/**/*.vue'
54-
- './packages/app/src/**/*.vue'
55-
- './packages/launchpad/src/**/*.vue'
53+
- './packages/frontend-shared/src/gql-components/**/*.{vue,ts,tsx,js,jsx}'
54+
- './packages/app/src/**/*.{vue,ts,tsx,js,jsx}'
55+
- './packages/launchpad/src/**/*.{vue,ts,tsx,js,jsx}'
5656
plugins:
5757
- add:
5858
content: '/* eslint-disable */'
@@ -92,23 +92,23 @@ generates:
9292
- 'typescript'
9393

9494
###
95-
# All of the GraphQL Query/Mutation documents we import for use in the .vue
95+
# All of the GraphQL Query/Mutation documents we import for use in the .{vue,ts,tsx,js,jsx}
9696
# files for useQuery / useMutation, as well as types associated with the fragments
9797
###
9898
'./packages/launchpad/src/generated/graphql.ts':
9999
documents:
100-
- './packages/launchpad/src/**/*.vue'
101-
- './packages/frontend-shared/src/**/*.vue'
100+
- './packages/launchpad/src/**/*.{vue,ts,tsx,js,jsx}'
101+
- './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}'
102102
<<: *vueOperations
103103

104104
'./packages/app/src/generated/graphql.ts':
105105
documents:
106-
- './packages/app/src/**/*.vue'
107-
- './packages/frontend-shared/src/**/*.vue'
106+
- './packages/app/src/**/*.{vue,ts,tsx,js,jsx}'
107+
- './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}'
108108
<<: *vueOperations
109109

110110
'./packages/frontend-shared/src/generated/graphql.ts':
111-
documents: './packages/frontend-shared/src/gql-components/**/*.vue'
111+
documents: './packages/frontend-shared/src/gql-components/**/*.{vue,ts,tsx,js,jsx}'
112112
<<: *vueOperations
113113
###
114114
# All GraphQL documents imported into the .spec.tsx files for component testing.
@@ -117,16 +117,16 @@ generates:
117117
###
118118
'./packages/launchpad/src/generated/graphql-test.ts':
119119
documents:
120-
- './packages/launchpad/src/**/*.vue'
121-
- './packages/frontend-shared/src/**/*.vue'
120+
- './packages/launchpad/src/**/*.{vue,ts,tsx,js,jsx}'
121+
- './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}'
122122
<<: *vueTesting
123123

124124
'./packages/app/src/generated/graphql-test.ts':
125125
documents:
126-
- './packages/app/src/**/*.vue'
127-
- './packages/frontend-shared/src/**/*.vue'
126+
- './packages/app/src/**/*.{vue,ts,tsx,js,jsx}'
127+
- './packages/frontend-shared/src/**/*.{vue,ts,tsx,js,jsx}'
128128
<<: *vueTesting
129129

130130
'./packages/frontend-shared/src/generated/graphql-test.ts':
131-
documents: './packages/frontend-shared/src/gql-components/**/*.vue'
131+
documents: './packages/frontend-shared/src/gql-components/**/*.{vue,ts,tsx,js,jsx}'
132132
<<: *vueTesting

packages/frontend-shared/.windicss/colors.ts

+4
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,10 @@ export const cyColors = {
166166
...customColors.red,
167167
DEFAULT: customColors.red[500],
168168
},
169+
info: {
170+
...customColors.indigo,
171+
DEFAULT: customColors.indigo[500],
172+
},
169173
warning: {
170174
...customColors.orange,
171175
DEFAULT: customColors.orange[500],

packages/frontend-shared/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
"generate-shiki-theme": "node ./script/generate-shiki-theme.js"
1616
},
1717
"dependencies": {
18+
"@toycode/markdown-it-class": "1.2.4",
19+
"markdown-it": "12.2.0",
1820
"shiki": "^0.9.12"
1921
},
2022
"devDependencies": {
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
import CoffeeIcon from '~icons/mdi/coffee'
2+
import LoadingIcon from '~icons/mdi/loading'
3+
import faker from 'faker'
4+
import Alert from './Alert.vue'
5+
import { defaultMessages } from '../locales/i18n'
6+
import { ref } from 'vue'
7+
8+
const messages = defaultMessages.components.alert
9+
10+
const alertBodySelector = '[data-testid=alert-body]'
11+
const alertHeaderSelector = '[data-testid=alert-header]'
12+
13+
const suffixIconSelector = '[data-testid=alert-suffix-icon]'
14+
const prefixIconSelector = '[data-testid=alert-prefix-icon]'
15+
16+
// This divider should eventually be tested inside of a visual regression test.
17+
const dividerLineSelector = '[data-testid=alert-body-divider]'
18+
const dismissSelector = `[aria-label=${messages.dismissAriaLabel}]`
19+
20+
const alertTitle = faker.hacker.phrase()
21+
const alertBodyContent = faker.lorem.sentences(2)
22+
23+
const makeDismissibleProps = () => {
24+
const modelValue = ref(true)
25+
const methods = {
26+
'onUpdate:modelValue': (newValue) => {
27+
modelValue.value = newValue
28+
},
29+
}
30+
31+
return { modelValue, methods }
32+
}
33+
34+
const prefixIcon = () => <CoffeeIcon data-testid="coffee-icon"/>
35+
const suffixIcon = () => <LoadingIcon data-testid="loading-icon" class="animate-spin"/>
36+
37+
describe('<Alert />', () => {
38+
describe('classes', () => {
39+
it('can change the text and background color for the alert', () => {
40+
cy.mount(() => <Alert headerClass="underline text-pink-500" alertClass="bg-pink-100" icon={suffixIcon}/>)
41+
})
42+
})
43+
44+
describe('prefix', () => {
45+
it('renders the icon prop as a prefix', () => {
46+
cy.mount(() => <Alert status="info" icon={CoffeeIcon} />)
47+
.get(prefixIconSelector).should('be.visible')
48+
})
49+
50+
it('renders the prefixIcon slot', () => {
51+
cy.mount(() => <Alert v-slots={{ prefixIcon }} />)
52+
.get('[data-testid=coffee-icon]').should('be.visible')
53+
})
54+
55+
it('renders the prefixIcon slot even when an icon is passed in', () => {
56+
cy.mount(() => (<Alert
57+
v-slots={{ prefixIcon }}
58+
icon={() => <LoadingIcon data-testid="loading-icon" />}
59+
/>))
60+
.get('[data-testid=coffee-icon]').should('be.visible')
61+
.get('[data-testid=loading-icon]').should('not.exist')
62+
})
63+
})
64+
65+
describe('suffix', () => {
66+
it('renders the suffixIcon slot', () => {
67+
cy.mount(() => <Alert title="Alert" v-slots={{ suffixIcon }} />)
68+
.get('[data-testid=loading-icon]').should('be.visible')
69+
})
70+
})
71+
72+
describe('static', () => {
73+
it('renders any body content and is open by default', () => {
74+
cy.mount(() => (
75+
<div class="space-y-2 text-center p-4">
76+
<Alert title="Alert">
77+
<p data-testid="body-content">{ faker.lorem.paragraphs(5) }</p>
78+
</Alert>
79+
</div>
80+
))
81+
82+
cy.get('[data-testid=body-content]').should('be.visible')
83+
})
84+
})
85+
86+
describe('dismissible', () => {
87+
it('renders any body content and is open by default', () => {
88+
const { modelValue, methods } = makeDismissibleProps()
89+
90+
cy.mount(() => (
91+
<div class="space-y-2 text-center p-4">
92+
<Alert title="Alert" dismissible modelValue={modelValue.value} {...methods}>
93+
<p data-testid="body-content">{ faker.lorem.paragraphs(5) }</p>
94+
</Alert>
95+
</div>
96+
))
97+
98+
cy.get('[data-testid=body-content]').should('be.visible')
99+
})
100+
101+
it('cannot be collapsed', () => {
102+
const { modelValue, methods } = makeDismissibleProps()
103+
104+
cy.mount(() => (<Alert title="Alert" dismissible modelValue={modelValue.value} {...methods}>
105+
<p data-testid="body-content">{ faker.lorem.paragraphs(5) }</p>
106+
</Alert>))
107+
.get(alertBodySelector).should('be.visible')
108+
.get(alertHeaderSelector).click()
109+
.get(alertBodySelector).should('be.visible')
110+
})
111+
112+
it('has a "dismiss" suffixIcon by default', () => {
113+
const { modelValue, methods } = makeDismissibleProps()
114+
115+
cy.mount(() => (
116+
<div class="space-y-2 text-center p-4">
117+
<Alert title="Alert" status="info" dismissible modelValue={modelValue.value} {...methods}/>
118+
</div>
119+
))
120+
121+
cy.get(suffixIconSelector)
122+
.should('be.visible')
123+
.and('have.attr', 'aria-label', messages.dismissAriaLabel)
124+
})
125+
126+
it('can be dismissed', () => {
127+
const { modelValue, methods } = makeDismissibleProps()
128+
129+
cy.mount(() => (<div class="space-y-2 text-center p-4">
130+
<Alert title="Alert" dismissible modelValue={modelValue.value} {...methods}/>
131+
</div>))
132+
133+
cy.get(suffixIconSelector).focus().click()
134+
.get(alertHeaderSelector).should('not.exist')
135+
})
136+
137+
it('accepts a custom dismiss icon, via slot', () => {
138+
const { modelValue, methods } = makeDismissibleProps()
139+
140+
cy.mount(() => <Alert title="Alert" dismissible v-slots={{ suffixIcon }} modelValue={modelValue.value} {...methods}/>)
141+
})
142+
143+
it('can create a dismiss button via the suffixIcons slot props', () => {
144+
const { modelValue, methods } = makeDismissibleProps()
145+
const slots = {
146+
suffixIcon ({ onClick, ariaLabel }) {
147+
return <CoffeeIcon onClick={onClick} aria-label={ariaLabel}/>
148+
},
149+
}
150+
151+
cy.mount(() => <Alert title="Alert" dismissible v-slots={slots} modelValue={modelValue.value} {...methods} />)
152+
cy.get(dismissSelector).click()
153+
cy.get(alertHeaderSelector).should('not.exist')
154+
})
155+
})
156+
157+
describe('with body content', () => {
158+
it('shows the body content initially', () => {
159+
const types = [{ 'dismissible': true }, { 'static': true }] as const
160+
161+
const alerts = types.map((type) => {
162+
const { modelValue, methods } = makeDismissibleProps()
163+
164+
return (<div>
165+
<h2 class="capitalize">{Object.keys(type)[0]}</h2>
166+
<Alert title={alertTitle} modelValue={modelValue.value} {...type} {...methods}>
167+
<p>{ faker.lorem.paragraphs(2) }</p>
168+
</Alert>
169+
</div>)
170+
})
171+
172+
cy.mount(() => <div class="space-y-2 p-4">{ alerts }</div>)
173+
cy.get(alertBodySelector).each((el) => cy.wrap(el).should('be.visible').get(dividerLineSelector).should('be.visible'))
174+
})
175+
})
176+
177+
describe('without body content', () => {
178+
it('can be dismissed', () => {
179+
const { modelValue, methods } = makeDismissibleProps()
180+
181+
cy.mount(<Alert collapsible title={alertTitle} modelValue={modelValue.value} {...methods}/>)
182+
cy.get(alertBodySelector).should('not.exist')
183+
cy.get(alertHeaderSelector).click().get(alertBodySelector).should('not.exist')
184+
})
185+
186+
it('renders each alert type without the divider line', () => {
187+
const types = [{ 'dismissible': true }, { 'collapsible': true }, { 'static': true }]
188+
189+
const alerts = types.map((type) => (<div>
190+
<h2 class="capitalize">{Object.keys(type)[0]}</h2>
191+
<Alert {...type} title={alertTitle} />
192+
</div>
193+
))
194+
195+
cy.mount(() => <div class="space-y-2 p-4">{ alerts }</div>)
196+
cy.get(alertHeaderSelector).each((el) => cy.wrap(el).click()).get(dividerLineSelector).should('not.exist')
197+
})
198+
})
199+
200+
describe('collapsible', () => {
201+
beforeEach(() => {
202+
cy.mount(() => (
203+
<div class="space-y-2 text-center p-4">
204+
<Alert status="success" collapsible icon={CoffeeIcon} title={alertTitle}>{alertBodyContent}</Alert>
205+
</div>
206+
))
207+
})
208+
209+
it('should not have a suffix icon', () => {
210+
cy.get(suffixIconSelector).should('not.exist')
211+
cy.get(dismissSelector).should('not.exist')
212+
})
213+
214+
it('renders the alert title', () => {
215+
cy.get(alertHeaderSelector).should('be.visible').and('have.text', alertTitle)
216+
})
217+
218+
it('the body content is collapsed by default', () => {
219+
cy.get(alertBodySelector).should('not.exist')
220+
})
221+
222+
it('can be expanded and collapsed to show the body content', () => {
223+
cy.get(alertHeaderSelector).click()
224+
cy.get(alertBodySelector).should('be.visible').and('have.text', alertBodyContent)
225+
cy.get(alertHeaderSelector).click()
226+
cy.get(alertBodySelector).should('not.exist')
227+
})
228+
})
229+
})
230+
231+
describe('playground', () => {
232+
it('renders', () => {
233+
const { modelValue, methods } = makeDismissibleProps()
234+
235+
cy.mount(() => {
236+
return (
237+
<div class="space-y-2 text-center p-4">
238+
<Alert status="success" collapsible icon={CoffeeIcon} title="Coffee, please">
239+
Delicious. Yum.
240+
<button class="bg-white rounded ml-2 px-2">Focusable</button>
241+
</Alert>
242+
<Alert status="info" collapsible title="An info alert">Just letting you know what's up.</Alert>
243+
<Alert status="warning">Nothing good is happening here!</Alert>
244+
<Alert icon={CoffeeIcon}
245+
dismissible
246+
status="error"
247+
modelValue={modelValue.value}
248+
{...methods}>Close me, please!</Alert>
249+
<Alert v-slots={{ suffixIcon }} collapsible status="default">A notice.</Alert>
250+
<Alert>Default alert</Alert>
251+
</div>
252+
)
253+
})
254+
})
255+
})

0 commit comments

Comments
 (0)