|
1 | | -import { describe, expect, it } from 'vitest'; |
| 1 | +import { Component, type ReactNode } from 'react'; |
| 2 | +import { describe, expect, it, vi } from 'vitest'; |
2 | 3 | import { page } from 'vitest/browser'; |
3 | 4 |
|
4 | 5 | import DsWorkspace from '../ds-workspace'; |
@@ -187,4 +188,217 @@ describe('DsWorkspace', () => { |
187 | 188 | expect(dialogRect.top).toBeGreaterThanOrEqual(headerRect.bottom); |
188 | 189 | expect(dialogRect.bottom).toBeLessThanOrEqual(footerRect.top + 1); |
189 | 190 | }); |
| 191 | + |
| 192 | + it('applies content-area layout on Content', async () => { |
| 193 | + await page.render( |
| 194 | + <DsWorkspace fillParent> |
| 195 | + <DsWorkspace.Content data-testid="content"> |
| 196 | + <span>Content area</span> |
| 197 | + </DsWorkspace.Content> |
| 198 | + </DsWorkspace>, |
| 199 | + ); |
| 200 | + |
| 201 | + const content = page.getByTestId('content').element(); |
| 202 | + const style = getComputedStyle(content); |
| 203 | + |
| 204 | + expect(content.className).not.toMatch(/contentWithLeftPanel/); |
| 205 | + expect(style.flexDirection).toBe('column'); |
| 206 | + expect(style.paddingLeft).toBe('40px'); |
| 207 | + expect(style.paddingRight).toBe('40px'); |
| 208 | + expect(style.paddingTop).toBe('24px'); |
| 209 | + expect(style.paddingBottom).toBe('24px'); |
| 210 | + expect(style.gap).toBe('16px'); |
| 211 | + }); |
| 212 | + |
| 213 | + describe('extended shell', () => { |
| 214 | + it('applies content-area margins when Body contains only Content', async () => { |
| 215 | + await page.render( |
| 216 | + <DsWorkspace fillParent> |
| 217 | + <DsWorkspace.Body> |
| 218 | + <DsWorkspace.Content data-testid="content"> |
| 219 | + <span>Content area</span> |
| 220 | + </DsWorkspace.Content> |
| 221 | + </DsWorkspace.Body> |
| 222 | + </DsWorkspace>, |
| 223 | + ); |
| 224 | + |
| 225 | + const content = page.getByTestId('content').element(); |
| 226 | + const style = getComputedStyle(content); |
| 227 | + |
| 228 | + expect(content.className).not.toMatch(/contentWithLeftPanel/); |
| 229 | + expect(style.paddingLeft).toBe('40px'); |
| 230 | + expect(style.paddingRight).toBe('40px'); |
| 231 | + expect(style.paddingTop).toBe('24px'); |
| 232 | + expect(style.paddingBottom).toBe('24px'); |
| 233 | + expect(style.gap).toBe('16px'); |
| 234 | + }); |
| 235 | + |
| 236 | + it('applies reduced horizontal margins when LeftPanel is mounted', async () => { |
| 237 | + await page.render( |
| 238 | + <DsWorkspace fillParent> |
| 239 | + <DsWorkspace.Body> |
| 240 | + <DsWorkspace.LeftPanel> |
| 241 | + <span>Left panel</span> |
| 242 | + </DsWorkspace.LeftPanel> |
| 243 | + <DsWorkspace.Content data-testid="content"> |
| 244 | + <span>Content area</span> |
| 245 | + </DsWorkspace.Content> |
| 246 | + </DsWorkspace.Body> |
| 247 | + </DsWorkspace>, |
| 248 | + ); |
| 249 | + |
| 250 | + const content = page.getByTestId('content').element(); |
| 251 | + const style = getComputedStyle(content); |
| 252 | + |
| 253 | + expect(content.className).toMatch(/contentWithLeftPanel/); |
| 254 | + expect(style.paddingLeft).toBe('24px'); |
| 255 | + expect(style.paddingRight).toBe('24px'); |
| 256 | + }); |
| 257 | + |
| 258 | + it('toggles Content margin class when LeftPanel is mounted and unmounted', async () => { |
| 259 | + const WorkspaceWithOptionalLeftPanel = ({ showLeftPanel }: { showLeftPanel: boolean }) => ( |
| 260 | + <DsWorkspace fillParent> |
| 261 | + <DsWorkspace.Body> |
| 262 | + {showLeftPanel ? ( |
| 263 | + <DsWorkspace.LeftPanel> |
| 264 | + <span>Left panel</span> |
| 265 | + </DsWorkspace.LeftPanel> |
| 266 | + ) : null} |
| 267 | + <DsWorkspace.Content data-testid="content"> |
| 268 | + <span>Content area</span> |
| 269 | + </DsWorkspace.Content> |
| 270 | + </DsWorkspace.Body> |
| 271 | + </DsWorkspace> |
| 272 | + ); |
| 273 | + |
| 274 | + const { rerender } = await page.render(<WorkspaceWithOptionalLeftPanel showLeftPanel={false} />); |
| 275 | + |
| 276 | + let content = page.getByTestId('content').element(); |
| 277 | + expect(content.className).not.toMatch(/contentWithLeftPanel/); |
| 278 | + expect(getComputedStyle(content).paddingLeft).toBe('40px'); |
| 279 | + |
| 280 | + await rerender(<WorkspaceWithOptionalLeftPanel showLeftPanel={true} />); |
| 281 | + |
| 282 | + content = page.getByTestId('content').element(); |
| 283 | + expect(content.className).toMatch(/contentWithLeftPanel/); |
| 284 | + expect(getComputedStyle(content).paddingLeft).toBe('24px'); |
| 285 | + |
| 286 | + await rerender(<WorkspaceWithOptionalLeftPanel showLeftPanel={false} />); |
| 287 | + |
| 288 | + content = page.getByTestId('content').element(); |
| 289 | + expect(content.className).not.toMatch(/contentWithLeftPanel/); |
| 290 | + expect(getComputedStyle(content).paddingLeft).toBe('40px'); |
| 291 | + }); |
| 292 | + |
| 293 | + it('does not change Content horizontal margins when SideMenu is present', async () => { |
| 294 | + await page.render( |
| 295 | + <DsWorkspace fillParent> |
| 296 | + <DsWorkspace.Body> |
| 297 | + <DsWorkspace.SideMenu> |
| 298 | + <span>Nav</span> |
| 299 | + </DsWorkspace.SideMenu> |
| 300 | + <DsWorkspace.Content data-testid="content"> |
| 301 | + <span>Content area</span> |
| 302 | + </DsWorkspace.Content> |
| 303 | + </DsWorkspace.Body> |
| 304 | + </DsWorkspace>, |
| 305 | + ); |
| 306 | + |
| 307 | + const content = page.getByTestId('content').element(); |
| 308 | + const style = getComputedStyle(content); |
| 309 | + |
| 310 | + expect(content.className).not.toMatch(/contentWithLeftPanel/); |
| 311 | + expect(style.paddingLeft).toBe('40px'); |
| 312 | + expect(style.paddingRight).toBe('40px'); |
| 313 | + }); |
| 314 | + |
| 315 | + it('content in Body creates a stacking context for drawer containment', async () => { |
| 316 | + await page.render( |
| 317 | + <DsWorkspace fillParent> |
| 318 | + <DsWorkspace.Body> |
| 319 | + <DsWorkspace.Content data-testid="content"> |
| 320 | + <span>Content area</span> |
| 321 | + </DsWorkspace.Content> |
| 322 | + </DsWorkspace.Body> |
| 323 | + </DsWorkspace>, |
| 324 | + ); |
| 325 | + |
| 326 | + const contentEl = page.getByTestId('content').element(); |
| 327 | + const style = getComputedStyle(contentEl); |
| 328 | + |
| 329 | + expect(style.position).toBe('relative'); |
| 330 | + }); |
| 331 | + |
| 332 | + it('drawer inside Body Content is positioned below header', async () => { |
| 333 | + await page.render( |
| 334 | + <div style={{ height: 500 }}> |
| 335 | + <DsWorkspace fillParent> |
| 336 | + <DsWorkspace.Header> |
| 337 | + <span>Header</span> |
| 338 | + </DsWorkspace.Header> |
| 339 | + <DsWorkspace.Body> |
| 340 | + <DsWorkspace.Content> |
| 341 | + <div style={{ position: 'absolute', inset: 0 }} role="dialog" aria-label="Drawer"> |
| 342 | + Drawer |
| 343 | + </div> |
| 344 | + <span>Main area</span> |
| 345 | + </DsWorkspace.Content> |
| 346 | + </DsWorkspace.Body> |
| 347 | + <DsWorkspace.Footer> |
| 348 | + <span>Footer</span> |
| 349 | + </DsWorkspace.Footer> |
| 350 | + </DsWorkspace> |
| 351 | + </div>, |
| 352 | + ); |
| 353 | + |
| 354 | + const headerRect = page.getByRole('banner').element().getBoundingClientRect(); |
| 355 | + const dialogRect = page.getByRole('dialog').element().getBoundingClientRect(); |
| 356 | + const footerRect = page.getByRole('contentinfo').element().getBoundingClientRect(); |
| 357 | + |
| 358 | + expect(dialogRect.top).toBeGreaterThanOrEqual(headerRect.bottom); |
| 359 | + expect(dialogRect.bottom).toBeLessThanOrEqual(footerRect.top + 1); |
| 360 | + }); |
| 361 | + |
| 362 | + it('throws when extended compound parts are used outside DsWorkspace', async () => { |
| 363 | + class TestErrorBoundary extends Component< |
| 364 | + { children: ReactNode; onError: (error: Error) => void }, |
| 365 | + { error: Error | null } |
| 366 | + > { |
| 367 | + override state = { error: null as Error | null }; |
| 368 | + |
| 369 | + static getDerivedStateFromError(error: Error) { |
| 370 | + return { error }; |
| 371 | + } |
| 372 | + |
| 373 | + override componentDidCatch(error: Error) { |
| 374 | + this.props.onError(error); |
| 375 | + } |
| 376 | + |
| 377 | + override render() { |
| 378 | + if (this.state.error) { |
| 379 | + return null; |
| 380 | + } |
| 381 | + |
| 382 | + return this.props.children; |
| 383 | + } |
| 384 | + } |
| 385 | + |
| 386 | + const onError = vi.fn<(error: Error) => void>(); |
| 387 | + |
| 388 | + await page.render( |
| 389 | + <TestErrorBoundary onError={onError}> |
| 390 | + <DsWorkspace.Body> |
| 391 | + <DsWorkspace.Content> |
| 392 | + <span>Content only</span> |
| 393 | + </DsWorkspace.Content> |
| 394 | + </DsWorkspace.Body> |
| 395 | + </TestErrorBoundary>, |
| 396 | + ); |
| 397 | + |
| 398 | + expect(onError).toHaveBeenCalledOnce(); |
| 399 | + expect(onError.mock.calls[0]?.[0]?.message).toBe( |
| 400 | + 'DsWorkspace compound components must be used within DsWorkspace', |
| 401 | + ); |
| 402 | + }); |
| 403 | + }); |
190 | 404 | }); |
0 commit comments